mockaton 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-dashboard.png +0 -0
- package/README.md +60 -59
- package/package.json +1 -1
- package/sample-mocks/api/user/links.GET.200.js +2 -1
- package/src/Api.js +16 -12
- package/src/Config.js +1 -1
- package/src/Dashboard.css +31 -32
- package/src/Dashboard.js +23 -26
- package/src/MockDispatcher.js +2 -2
- package/src/mockBrokersCollection.js +2 -1
- package/src/utils/http-response.js +5 -0
package/README-dashboard.png
CHANGED
|
Binary file
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Mockaton
|
|
2
2
|
_Mockaton_ is a mock server for developing and testing frontends.
|
|
3
3
|
|
|
4
|
-
It scans
|
|
4
|
+
It scans a given directory for files following a specific
|
|
5
5
|
file name convention, which is similar to the URL paths. For
|
|
6
6
|
example, the following file will be served for `/api/user/1234`
|
|
7
7
|
```
|
|
@@ -13,60 +13,16 @@ extension](https://github.com/ericfortis/devtools-ext-tar-http-requests) can
|
|
|
13
13
|
be used for downloading a TAR of your XHR requests following that convention.
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
### Mock Variants
|
|
17
|
-
Each route can have many mocks, which could either be:
|
|
18
|
-
- Different response __status code__.
|
|
19
|
-
- e.g. for testing error responses.
|
|
20
|
-
- __Comment__ on the filename, which is anything within parentheses.
|
|
21
|
-
- e.g. `api/user(my-comment).POST.201.json`
|
|
22
|
-
|
|
23
|
-
Those alternatives can be manually selected in the dashboard
|
|
24
|
-
UI, or programmatically, for instance, for setting up tests.
|
|
25
|
-
|
|
26
|
-
The first file in **alphabetical order** becomes the default mock.
|
|
27
|
-
|
|
28
|
-
### Optionally, you can write mocks in JavaScript
|
|
29
|
-
An Object, Array, or String is sent as JSON.
|
|
30
|
-
|
|
31
|
-
`api/foo.GET.200.js`
|
|
32
|
-
```js
|
|
33
|
-
export default [
|
|
34
|
-
{ id: 0 }
|
|
35
|
-
]
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Or, export default a function. There, you
|
|
39
|
-
can override the response status and the default JSON content
|
|
40
|
-
type. But don’t call `response.end()`, just return a string.
|
|
41
|
-
```js
|
|
42
|
-
export default function (req, response) {
|
|
43
|
-
return JSON.stringify({ a: 1 })
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
### Proxying Routes
|
|
49
|
-
`Config.proxyFallback` lets you specify a target
|
|
50
|
-
server for serving routes you don’t have mocks for.
|
|
51
|
-
|
|
52
|
-
|
|
53
16
|
## Getting Started
|
|
54
17
|
The best way to learn _Mockaton_ is by checking out this repo and
|
|
55
18
|
exploring its [sample-mocks/](./sample-mocks) directory. Then, run
|
|
56
19
|
[`./_usage_example.js`](./_usage_example.js) and you’ll see this dashboard:
|
|
57
20
|
|
|
58
|
-
<img src="./README-dashboard.png" style="max-width:890px"/>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
## Delay 🕓
|
|
62
|
-
The clock icon next to the mock selector is a checkbox for delaying a
|
|
63
|
-
particular response. They are handy for testing spinners.
|
|
64
21
|
|
|
65
|
-
|
|
22
|
+
<img src="./README-dashboard.png" style="max-width:820px"/>
|
|
66
23
|
|
|
67
|
-
---
|
|
68
24
|
|
|
69
|
-
## Basic Usage
|
|
25
|
+
## Basic Usage
|
|
70
26
|
```
|
|
71
27
|
npm install mockaton
|
|
72
28
|
```
|
|
@@ -101,8 +57,54 @@ interface Config {
|
|
|
101
57
|
extraHeaders?: []
|
|
102
58
|
}
|
|
103
59
|
```
|
|
60
|
+
|
|
104
61
|
---
|
|
105
62
|
|
|
63
|
+
## Mock Variants
|
|
64
|
+
Each route can have many mocks, which could either be:
|
|
65
|
+
- Different response __status code__.
|
|
66
|
+
- e.g. for testing error responses.
|
|
67
|
+
- __Comment__ on the filename, which is anything within parentheses.
|
|
68
|
+
- e.g. `api/user(my-comment).POST.201.json`
|
|
69
|
+
|
|
70
|
+
Those alternatives can be manually selected in the dashboard
|
|
71
|
+
UI, or programmatically, for instance, for setting up tests.
|
|
72
|
+
|
|
73
|
+
The first file in **alphabetical order** becomes the default mock.
|
|
74
|
+
|
|
75
|
+
## You can write JSON mocks in JavaScript
|
|
76
|
+
An Object, Array, or String is sent as JSON.
|
|
77
|
+
|
|
78
|
+
`api/foo.GET.200.js`
|
|
79
|
+
```js
|
|
80
|
+
export default [
|
|
81
|
+
{ id: 0 }
|
|
82
|
+
]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or, export default a function. There, you
|
|
86
|
+
can override the response status and the default JSON content
|
|
87
|
+
type. But don’t call `response.end()`, just return a string.
|
|
88
|
+
```js
|
|
89
|
+
export default function (req, response) {
|
|
90
|
+
return JSON.stringify({ a: 1 })
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## Proxying Routes
|
|
96
|
+
`Config.proxyFallback` lets you specify a target
|
|
97
|
+
server for serving routes you don’t have mocks for.
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
## Delay 🕓
|
|
102
|
+
The clock icon next to the mock selector is a checkbox for delaying a
|
|
103
|
+
particular response. They are handy for testing spinners.
|
|
104
|
+
|
|
105
|
+
The delay is globally configurable via `Config.delay = 1200` (milliseconds).
|
|
106
|
+
|
|
107
|
+
|
|
106
108
|
## File Name Convention
|
|
107
109
|
|
|
108
110
|
|
|
@@ -125,7 +127,7 @@ Comments are anything within parentheses, including them.
|
|
|
125
127
|
They are ignored for URL purposes, so they have no effect
|
|
126
128
|
on the URL mask. For example, these two are for `/api/foo`
|
|
127
129
|
<pre>
|
|
128
|
-
api/foo<b>(my comment)</b>.GET.200.json<b>(
|
|
130
|
+
api/foo<b>(my comment)</b>.GET.200.json<b>(bar)</b>
|
|
129
131
|
api/foo.GET.200.json
|
|
130
132
|
</pre>
|
|
131
133
|
|
|
@@ -143,10 +145,10 @@ but since that’s part of the query string, it’s ignored anyway.
|
|
|
143
145
|
|
|
144
146
|
|
|
145
147
|
|
|
146
|
-
### Default (index-like)
|
|
147
|
-
For the default route of a directory, omit the
|
|
148
|
-
the extension). For example, the following files will be
|
|
149
|
-
to `api/foo` because comments and the query string are ignored.
|
|
148
|
+
### Default (index-like) route
|
|
149
|
+
For the default route of a directory, omit the mock filename name
|
|
150
|
+
(<b>just use the extension</b>). For example, the following files will be
|
|
151
|
+
routed to `api/foo` because comments and the query string are ignored.
|
|
150
152
|
```text
|
|
151
153
|
api/foo/.GET.200.json
|
|
152
154
|
api/foo/?bar=[bar].GET.200.json
|
|
@@ -160,7 +162,7 @@ The selected cookie is sent in every response in the `Set-Cookie` header.
|
|
|
160
162
|
import { jwtCookie } from 'mockaton'
|
|
161
163
|
|
|
162
164
|
Config.cookies = {
|
|
163
|
-
'My Admin User':
|
|
165
|
+
'My Admin User': 'my-cookie=1;Path=/;SameSite=strict',
|
|
164
166
|
'My Normal User': 'my-cookie=0;Path=/;SameSite=strict',
|
|
165
167
|
'My JWT': jwtCookie('my-cookie', {
|
|
166
168
|
email: 'john.doe@example.com',
|
|
@@ -175,15 +177,14 @@ words, it’s useful if you only care about its payload.
|
|
|
175
177
|
|
|
176
178
|
|
|
177
179
|
## `Config.extraHeaders`
|
|
178
|
-
They are applied last, right before ending the response. In
|
|
179
|
-
|
|
180
|
-
not tuples, the header name goes in even-numbered indices.
|
|
180
|
+
They are applied last, right before ending the response. In other words, they
|
|
181
|
+
can overwrite the `Content-Type`. The header name goes in even indices.
|
|
181
182
|
|
|
182
183
|
```js
|
|
183
184
|
Config.extraHeaders = [
|
|
184
185
|
'Server', 'Mockaton',
|
|
185
|
-
'Set-Cookie', '
|
|
186
|
-
'Set-Cookie', '
|
|
186
|
+
'Set-Cookie', 'foo=FOO;Path=/;SameSite=strict',
|
|
187
|
+
'Set-Cookie', 'bar=BAR;Path=/;SameSite=strict'
|
|
187
188
|
]
|
|
188
189
|
```
|
|
189
190
|
|
|
@@ -223,7 +224,7 @@ fetch(addr + '/mockaton/bulk-select-by-comment', {
|
|
|
223
224
|
### Reset
|
|
224
225
|
Re-Initialize the collection and its states (selected mocks and cookies, delays, etc.).
|
|
225
226
|
```js
|
|
226
|
-
fetch(
|
|
227
|
+
fetch(addr + '/mockaton/reset', {
|
|
227
228
|
method: 'PATCH'
|
|
228
229
|
})
|
|
229
230
|
```
|
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Config } from './Config.js'
|
|
|
9
9
|
import { DF, API } from './ApiConstants.js'
|
|
10
10
|
import { parseJSON } from './utils/http-request.js'
|
|
11
11
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
12
|
-
import { sendOK, sendBadRequest, sendJSON, sendFile } from './utils/http-response.js'
|
|
12
|
+
import { sendOK, sendBadRequest, sendJSON, sendFile, sendUnprocessableContent } from './utils/http-response.js'
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
export const apiGetRequests = new Map([
|
|
@@ -24,11 +24,11 @@ export const apiGetRequests = new Map([
|
|
|
24
24
|
])
|
|
25
25
|
|
|
26
26
|
export const apiPatchRequests = new Map([
|
|
27
|
-
[API.bulkSelect, bulkUpdateBrokersByCommentTag],
|
|
28
27
|
[API.edit, updateBroker],
|
|
29
28
|
[API.reset, reinitialize],
|
|
30
29
|
[API.cookies, selectCookie],
|
|
31
|
-
[API.fallback, updateProxyFallback]
|
|
30
|
+
[API.fallback, updateProxyFallback],
|
|
31
|
+
[API.bulkSelect, bulkUpdateBrokersByCommentTag]
|
|
32
32
|
])
|
|
33
33
|
|
|
34
34
|
function serveDashboard(_, response) {
|
|
@@ -48,6 +48,11 @@ function listMockBrokers(_, response) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
function reinitialize(_, response) {
|
|
52
|
+
mockBrokersCollection.init()
|
|
53
|
+
sendOK(response)
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
async function selectCookie(req, response) {
|
|
52
57
|
try {
|
|
53
58
|
cookie.setCurrent(await parseJSON(req))
|
|
@@ -59,15 +64,14 @@ async function selectCookie(req, response) {
|
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
function reinitialize(_, response) {
|
|
63
|
-
mockBrokersCollection.init()
|
|
64
|
-
sendOK(response)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
67
|
async function updateBroker(req, response) {
|
|
68
68
|
try {
|
|
69
69
|
const body = await parseJSON(req)
|
|
70
70
|
const broker = mockBrokersCollection.getBrokerByFilename(body[DF.file])
|
|
71
|
+
if (!broker) {
|
|
72
|
+
sendUnprocessableContent(response)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
71
75
|
if (DF.delayed in body)
|
|
72
76
|
broker.updateDelay(body[DF.delayed])
|
|
73
77
|
broker.updateFile(body[DF.file])
|
|
@@ -79,9 +83,9 @@ async function updateBroker(req, response) {
|
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
async function
|
|
86
|
+
async function updateProxyFallback(req, response) {
|
|
83
87
|
try {
|
|
84
|
-
|
|
88
|
+
Config.proxyFallback = await parseJSON(req)
|
|
85
89
|
sendOK(response)
|
|
86
90
|
}
|
|
87
91
|
catch (error) {
|
|
@@ -90,9 +94,9 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
async function
|
|
97
|
+
async function bulkUpdateBrokersByCommentTag(req, response) {
|
|
94
98
|
try {
|
|
95
|
-
|
|
99
|
+
mockBrokersCollection.setMocksMatchingComment(await parseJSON(req))
|
|
96
100
|
sendOK(response)
|
|
97
101
|
}
|
|
98
102
|
catch (error) {
|
package/src/Config.js
CHANGED
|
@@ -26,7 +26,7 @@ export function setup(options) {
|
|
|
26
26
|
delay: ms => Number.isInteger(ms) && ms > 0,
|
|
27
27
|
cookies: is(Object),
|
|
28
28
|
skipOpen: is(Boolean),
|
|
29
|
-
proxyFallback:
|
|
29
|
+
proxyFallback: optional(URL.canParse),
|
|
30
30
|
allowedExt: is(RegExp),
|
|
31
31
|
generate500: is(Boolean),
|
|
32
32
|
extraHeaders: Array.isArray
|
package/src/Dashboard.css
CHANGED
|
@@ -14,23 +14,13 @@ body {
|
|
|
14
14
|
padding: 16px;
|
|
15
15
|
}
|
|
16
16
|
* {
|
|
17
|
+
padding: 0;
|
|
17
18
|
border: 0;
|
|
18
19
|
margin: 0;
|
|
19
20
|
font-family: system-ui, sans-serif;
|
|
20
21
|
font-size: 100%;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
h1 {
|
|
24
|
-
padding: 12px 0;
|
|
25
|
-
margin: 0;
|
|
26
|
-
font-size: 2rem;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
select {
|
|
31
|
-
padding: 3px 0;
|
|
32
|
-
border: 1px solid #444;
|
|
33
|
-
}
|
|
34
24
|
|
|
35
25
|
fieldset {
|
|
36
26
|
width: 120px;
|
|
@@ -47,15 +37,6 @@ fieldset {
|
|
|
47
37
|
}
|
|
48
38
|
}
|
|
49
39
|
|
|
50
|
-
.CookieSelector {
|
|
51
|
-
display: flex;
|
|
52
|
-
align-items: center;
|
|
53
|
-
margin-left: 20px;
|
|
54
|
-
|
|
55
|
-
select {
|
|
56
|
-
margin-left: 5px;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
40
|
|
|
60
41
|
main {
|
|
61
42
|
display: flex;
|
|
@@ -72,14 +53,39 @@ main {
|
|
|
72
53
|
}
|
|
73
54
|
}
|
|
74
55
|
|
|
75
|
-
|
|
56
|
+
menu {
|
|
76
57
|
display: flex;
|
|
77
|
-
|
|
58
|
+
margin-bottom: 12px;
|
|
59
|
+
gap: 14px;
|
|
60
|
+
align-items: flex-end;
|
|
61
|
+
|
|
62
|
+
h1 {
|
|
63
|
+
margin: 0;
|
|
64
|
+
margin-right: 14px;
|
|
65
|
+
font-size: 2rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
label {
|
|
69
|
+
span {
|
|
70
|
+
display: block;
|
|
71
|
+
color: #555;
|
|
72
|
+
font-size: .85rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
select {
|
|
76
|
+
width: 143px;
|
|
77
|
+
padding: 3px 0;
|
|
78
|
+
border: 1px solid #bbb;
|
|
79
|
+
margin-top: 1px;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
border-radius: 4px;
|
|
82
|
+
font-size: 0.9rem;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
78
85
|
|
|
79
86
|
button {
|
|
80
|
-
padding:
|
|
87
|
+
padding: 4px 12px;
|
|
81
88
|
border: 1px solid var(--colorRed);
|
|
82
|
-
margin-left: 20px;
|
|
83
89
|
background: transparent;
|
|
84
90
|
color: var(--colorRed);
|
|
85
91
|
border-radius: 50px;
|
|
@@ -132,18 +138,11 @@ main {
|
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
140
|
|
|
135
|
-
.BulkSelectSection {
|
|
136
|
-
margin: 20px 0;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.BulkSelectSection select {
|
|
140
|
-
margin-top: 5px;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
141
|
.MockSelector {
|
|
144
142
|
width: 300px;
|
|
145
143
|
padding: 8px 1px;
|
|
146
144
|
border: 0;
|
|
145
|
+
border-radius: 4px;
|
|
147
146
|
background: #eee;
|
|
148
147
|
text-align: right;
|
|
149
148
|
direction: rtl;
|
package/src/Dashboard.js
CHANGED
|
@@ -16,14 +16,11 @@ const Strings = {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const CSS = {
|
|
19
|
-
BulkSelectSection: 'BulkSelectSection',
|
|
20
|
-
CookieSelector: 'CookieSelector',
|
|
21
19
|
DelayCheckbox: 'DelayCheckbox',
|
|
22
20
|
Documentation: 'Documentation',
|
|
23
21
|
MockSelector: 'MockSelector',
|
|
24
22
|
PayloadViewer: 'PayloadViewer',
|
|
25
23
|
PreviewLink: 'PreviewLink',
|
|
26
|
-
TitleWrap: 'TitleWrap',
|
|
27
24
|
|
|
28
25
|
bold: 'bold',
|
|
29
26
|
chosen: 'chosen',
|
|
@@ -57,13 +54,11 @@ function DevPanel(brokersByMethod, cookies, comments) {
|
|
|
57
54
|
document.title = Strings.title
|
|
58
55
|
return (
|
|
59
56
|
r('div', null,
|
|
60
|
-
r('
|
|
57
|
+
r('menu', null,
|
|
61
58
|
r('h1', null, Strings.title),
|
|
62
|
-
r(
|
|
63
|
-
r(
|
|
64
|
-
|
|
65
|
-
r('h2', null, Strings.bulk_select_by_comment),
|
|
66
|
-
r(BulkSelector, { comments })),
|
|
59
|
+
r(CookieSelector, { list: cookies }),
|
|
60
|
+
r(BulkSelector, { comments }),
|
|
61
|
+
r(ResetButton)),
|
|
67
62
|
r('main', null,
|
|
68
63
|
r('table', null, Object.entries(brokersByMethod).map(([method, brokers]) =>
|
|
69
64
|
r(SectionByMethod, { method, brokers }))),
|
|
@@ -88,8 +83,8 @@ function ResetButton() {
|
|
|
88
83
|
|
|
89
84
|
function CookieSelector({ list }) {
|
|
90
85
|
return (
|
|
91
|
-
r('label',
|
|
92
|
-
Strings.cookie,
|
|
86
|
+
r('label', null,
|
|
87
|
+
r('span', null, Strings.cookie),
|
|
93
88
|
r('select', {
|
|
94
89
|
autocomplete: 'off',
|
|
95
90
|
disabled: list.length <= 1,
|
|
@@ -111,21 +106,23 @@ function CookieSelector({ list }) {
|
|
|
111
106
|
|
|
112
107
|
function BulkSelector({ comments }) {
|
|
113
108
|
return (
|
|
114
|
-
r('
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
109
|
+
r('label', null,
|
|
110
|
+
r('span', null, Strings.bulk_select_by_comment),
|
|
111
|
+
r('select', {
|
|
112
|
+
autocomplete: 'off',
|
|
113
|
+
disabled: comments.length <= 1,
|
|
114
|
+
onChange() {
|
|
115
|
+
fetch(API.bulkSelect, {
|
|
116
|
+
method: 'PATCH',
|
|
117
|
+
body: JSON.stringify(this.value)
|
|
118
|
+
})
|
|
119
|
+
.then(init)
|
|
120
|
+
.catch(console.error)
|
|
121
|
+
}
|
|
122
|
+
}, [Strings.select_one].concat(comments).map(item =>
|
|
123
|
+
r('option', {
|
|
124
|
+
value: item
|
|
125
|
+
}, item)))))
|
|
129
126
|
}
|
|
130
127
|
|
|
131
128
|
|
package/src/MockDispatcher.js
CHANGED
|
@@ -36,8 +36,8 @@ export async function dispatchMock(req, response) {
|
|
|
36
36
|
response.setHeader('content-type', mimeFor('.json'))
|
|
37
37
|
const jsExport = await importDefault(file)
|
|
38
38
|
mockText = typeof jsExport === 'function'
|
|
39
|
-
? jsExport(req, response)
|
|
40
|
-
: JSON.stringify(jsExport)
|
|
39
|
+
? await jsExport(req, response)
|
|
40
|
+
: JSON.stringify(jsExport, null, 2)
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
43
43
|
response.setHeader('content-type', mimeFor(file))
|
|
@@ -54,7 +54,8 @@ export const getAll = () => collection
|
|
|
54
54
|
|
|
55
55
|
export const getBrokerByFilename = file => {
|
|
56
56
|
const { method, urlMask } = Route.parseFilename(file)
|
|
57
|
-
|
|
57
|
+
if (collection[method])
|
|
58
|
+
return collection[method][urlMask]
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// Searching the routes in reverse order so dynamic params (e.g.
|
|
@@ -54,6 +54,11 @@ export function sendNotFound(response) {
|
|
|
54
54
|
response.end()
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export function sendUnprocessableContent(response) {
|
|
58
|
+
response.statusCode = 422
|
|
59
|
+
response.end()
|
|
60
|
+
}
|
|
61
|
+
|
|
57
62
|
export function sendInternalServerError(response) {
|
|
58
63
|
response.statusCode = 500
|
|
59
64
|
response.end()
|