Skip to content

Commit 46cfddb

Browse files
committed
test: add test cases for partially consumed and cancelled request body
1 parent 548ab07 commit 46cfddb

File tree

3 files changed

+125
-31
lines changed

3 files changed

+125
-31
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ serve({
128128
### `autoCleanupIncoming`
129129

130130
The default value is `true`. The Node.js Adapter automatically cleans up (explicitly call `destroy()` method) if application is not finished to consume the incoming request. If you don't want to do that, set `false`.
131-
If the application accepts connections from arbitrary clients, this cleanup must be done otherwise incomplete requests from clients may cause the application to stop responding. If your application only accepts connections from trusted clients, such as in a reverse proxy environment, you can improve performance by setting it to false.
131+
If the application accepts connections from arbitrary clients, this cleanup must be done otherwise incomplete requests from clients may cause the application to stop responding. If your application only accepts connections from trusted clients, such as in a reverse proxy environment and there is no process that returns a response without reading the body of the POST request all the way through, you can improve performance by setting it to false.
132132

133133
```ts
134134
serve({

test/app.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Response as PonyfillResponse } from '@whatwg-node/fetch'
2+
import { Hono } from 'hono'
3+
4+
export const app = new Hono()
5+
6+
app.get('/', (c) => c.text('Hello! Node!'))
7+
app.get('/url', (c) => c.text(c.req.url))
8+
9+
app.get('/posts', (c) => {
10+
return c.text(`Page ${c.req.query('page')}`)
11+
})
12+
app.get('/user-agent', (c) => {
13+
return c.text(c.req.header('user-agent') as string)
14+
})
15+
app.post('/posts', (c) => {
16+
return c.redirect('/posts')
17+
})
18+
app.post('/body-consumed', async (c) => {
19+
return c.text(`Body length: ${(await c.req.text()).length}`)
20+
})
21+
app.post('/no-body-consumed', (c) => {
22+
if (!c.req.raw.body) {
23+
// force create new request object
24+
throw new Error('No body consumed')
25+
}
26+
return c.text('No body consumed')
27+
})
28+
app.post('/body-cancelled', (c) => {
29+
if (!c.req.raw.body) {
30+
// force create new request object
31+
throw new Error('No body consumed')
32+
}
33+
c.req.raw.body.cancel()
34+
return c.text('Body cancelled')
35+
})
36+
app.post('/partially-consumed', async (c) => {
37+
if (!c.req.raw.body) {
38+
// force create new request object
39+
throw new Error('No body consumed')
40+
}
41+
const reader = c.req.raw.body.getReader()
42+
await reader.read() // read only one chunk
43+
return c.text('Partially consumed')
44+
})
45+
app.post('/partially-consumed-and-cancelled', async (c) => {
46+
if (!c.req.raw.body) {
47+
// force create new request object
48+
throw new Error('No body consumed')
49+
}
50+
const reader = c.req.raw.body.getReader()
51+
await reader.read() // read only one chunk
52+
reader.cancel()
53+
return c.text('Partially consumed and cancelled')
54+
})
55+
app.delete('/posts/:id', (c) => {
56+
return c.text(`DELETE ${c.req.param('id')}`)
57+
})
58+
// @ts-expect-error the response is string
59+
app.get('/invalid', () => {
60+
return '<h1>HTML</h1>'
61+
})
62+
app.get('/ponyfill', () => {
63+
return new PonyfillResponse('Pony')
64+
})
65+
66+
app.on('trace', '/', (c) => {
67+
const headers = c.req.raw.headers // build new request object
68+
return c.text(`headers: ${JSON.stringify(headers)}`)
69+
})

test/server.test.ts

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Response as PonyfillResponse } from '@whatwg-node/fetch'
21
import { Hono } from 'hono'
32
import { basicAuth } from 'hono/basic-auth'
43
import { compress } from 'hono/compress'
@@ -13,37 +12,9 @@ import { GlobalRequest, Request as LightweightRequest, getAbortController } from
1312
import { GlobalResponse, Response as LightweightResponse } from '../src/response'
1413
import { createAdaptorServer, serve } from '../src/server'
1514
import type { HttpBindings } from '../src/types'
15+
import { app } from './app'
1616

1717
describe('Basic', () => {
18-
const app = new Hono()
19-
app.get('/', (c) => c.text('Hello! Node!'))
20-
app.get('/url', (c) => c.text(c.req.url))
21-
22-
app.get('/posts', (c) => {
23-
return c.text(`Page ${c.req.query('page')}`)
24-
})
25-
app.get('/user-agent', (c) => {
26-
return c.text(c.req.header('user-agent') as string)
27-
})
28-
app.post('/posts', (c) => {
29-
return c.redirect('/posts')
30-
})
31-
app.delete('/posts/:id', (c) => {
32-
return c.text(`DELETE ${c.req.param('id')}`)
33-
})
34-
// @ts-expect-error the response is string
35-
app.get('/invalid', () => {
36-
return '<h1>HTML</h1>'
37-
})
38-
app.get('/ponyfill', () => {
39-
return new PonyfillResponse('Pony')
40-
})
41-
42-
app.on('trace', '/', (c) => {
43-
const headers = c.req.raw.headers // build new request object
44-
return c.text(`headers: ${JSON.stringify(headers)}`)
45-
})
46-
4718
const server = createAdaptorServer(app)
4819

4920
it('Should return 200 response - GET /', async () => {
@@ -82,6 +53,60 @@ describe('Basic', () => {
8253
expect(res.headers['location']).toBe('/posts')
8354
})
8455

56+
it('Should return 200 response - POST /no-body-consumed', async () => {
57+
const res = await request(server).post('/no-body-consumed').send('')
58+
expect(res.status).toBe(200)
59+
expect(res.text).toBe('No body consumed')
60+
})
61+
62+
it('Should return 200 response - POST /body-cancelled', async () => {
63+
const res = await request(server).post('/body-cancelled').send('')
64+
expect(res.status).toBe(200)
65+
expect(res.text).toBe('Body cancelled')
66+
})
67+
68+
it('Should return 200 response - POST /partially-consumed', async () => {
69+
const buffer = Buffer.alloc(1024 * 10) // large buffer
70+
const res = await new Promise<any>((resolve, reject) => {
71+
const req = request(server)
72+
.post('/partially-consumed')
73+
.set('Content-Length', buffer.length.toString())
74+
75+
req.write(buffer)
76+
req.end((err, res) => {
77+
if (err) {
78+
reject(err)
79+
} else {
80+
resolve(res)
81+
}
82+
})
83+
})
84+
85+
expect(res.status).toBe(200)
86+
expect(res.text).toBe('Partially consumed')
87+
})
88+
89+
it('Should return 200 response - POST /partially-consumed-and-cancelled', async () => {
90+
const buffer = Buffer.alloc(1) // A large buffer will not make the test go far, so keep it small because it won't go far.
91+
const res = await new Promise<any>((resolve, reject) => {
92+
const req = request(server)
93+
.post('/partially-consumed-and-cancelled')
94+
.set('Content-Length', buffer.length.toString())
95+
96+
req.write(buffer)
97+
req.end((err, res) => {
98+
if (err) {
99+
reject(err)
100+
} else {
101+
resolve(res)
102+
}
103+
})
104+
})
105+
106+
expect(res.status).toBe(200)
107+
expect(res.text).toBe('Partially consumed and cancelled')
108+
})
109+
85110
it('Should return 201 response - DELETE /posts/123', async () => {
86111
const res = await request(server).delete('/posts/123')
87112
expect(res.status).toBe(200)

0 commit comments

Comments
 (0)