Skip to content

Commit 1402d3d

Browse files
authored
Merge pull request #503 from AikidoSec/fix-hono-body-locked
Fix Hono response body locked error
2 parents f80214c + 37bf067 commit 1402d3d

File tree

4 files changed

+178
-38
lines changed

4 files changed

+178
-38
lines changed

library/sources/Hono.test.ts

Lines changed: 135 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,28 @@ function getApp() {
9494
return c.json(getContext());
9595
});
9696

97+
app.post("/json", async (c) => {
98+
try {
99+
const json = await c.req.json();
100+
} catch (e) {
101+
if (e instanceof SyntaxError) {
102+
return c.text("Invalid JSON", 400);
103+
}
104+
throw e;
105+
}
106+
return c.json(getContext());
107+
});
108+
109+
app.post("/text", async (c) => {
110+
const text = await c.req.text();
111+
return c.json(getContext());
112+
});
113+
114+
app.post("/form", async (c) => {
115+
const form = await c.req.parseBody();
116+
return c.json(getContext());
117+
});
118+
97119
app.on(["GET"], ["/user", "/user/blocked"], (c) => {
98120
return c.json(getContext());
99121
});
@@ -140,7 +162,7 @@ t.test("it adds context from request for GET", opts, async (t) => {
140162
});
141163

142164
t.test("it adds JSON body to context", opts, async (t) => {
143-
const response = await getApp().request("/", {
165+
const response = await getApp().request("/json", {
144166
method: "POST",
145167
headers: {
146168
"content-type": "application/json",
@@ -153,12 +175,12 @@ t.test("it adds JSON body to context", opts, async (t) => {
153175
method: "POST",
154176
body: { title: "test" },
155177
source: "hono",
156-
route: "/",
178+
route: "/json",
157179
});
158180
});
159181

160182
t.test("it adds form body to context", opts, async (t) => {
161-
const response = await getApp().request("/", {
183+
const response = await getApp().request("/form", {
162184
method: "POST",
163185
headers: {
164186
"content-type": "application/x-www-form-urlencoded",
@@ -171,12 +193,12 @@ t.test("it adds form body to context", opts, async (t) => {
171193
method: "POST",
172194
body: { title: "test" },
173195
source: "hono",
174-
route: "/",
196+
route: "/form",
175197
});
176198
});
177199

178200
t.test("it adds text body to context", opts, async (t) => {
179-
const response = await getApp().request("/", {
201+
const response = await getApp().request("/text", {
180202
method: "POST",
181203
headers: {
182204
"content-type": "text/plain",
@@ -189,12 +211,12 @@ t.test("it adds text body to context", opts, async (t) => {
189211
method: "POST",
190212
body: "test",
191213
source: "hono",
192-
route: "/",
214+
route: "/text",
193215
});
194216
});
195217

196218
t.test("it adds xml body to context", opts, async (t) => {
197-
const response = await getApp().request("/", {
219+
const response = await getApp().request("/text", {
198220
method: "POST",
199221
headers: {
200222
"content-type": "application/xml",
@@ -207,7 +229,7 @@ t.test("it adds xml body to context", opts, async (t) => {
207229
method: "POST",
208230
body: "<test>test</test>",
209231
source: "hono",
210-
route: "/",
232+
route: "/text",
211233
});
212234
});
213235

@@ -379,3 +401,108 @@ t.test("The hono async context still works", opts, async (t) => {
379401
const body = await response.text();
380402
t.equal(body, "test-value");
381403
});
404+
405+
t.test("Proxy request", opts, async (t) => {
406+
const { Hono } = require("hono") as typeof import("hono");
407+
const { serve } =
408+
require("@hono/node-server") as typeof import("@hono/node-server");
409+
410+
const app = new Hono();
411+
412+
app.on(["GET", "POST"], "/proxy", async (c) => {
413+
const response = await globalThis.fetch(
414+
new Request("http://127.0.0.1:8768/body", {
415+
method: c.req.method,
416+
headers: c.req.raw.headers,
417+
body: c.req.raw.body,
418+
// @ts-expect-error wrong types
419+
duplex: "half",
420+
redirect: "manual",
421+
})
422+
);
423+
// clone the response to return a response with modifiable headers
424+
return new Response(response.body, response);
425+
});
426+
427+
app.post("/body", async (c) => {
428+
return await c.req.json();
429+
});
430+
431+
const server = serve({
432+
fetch: app.fetch,
433+
port: 8767,
434+
hostname: "127.0.0.1",
435+
});
436+
437+
const app2 = new Hono();
438+
app2.all("/*", async (c) => {
439+
return c.text(await c.req.text());
440+
});
441+
442+
const server2 = serve({
443+
fetch: app2.fetch,
444+
port: 8768,
445+
hostname: "127.0.0.1",
446+
});
447+
448+
const response = await fetch.fetch({
449+
url: new URL("http://127.0.0.1:8767/proxy"),
450+
method: "POST",
451+
headers: {
452+
"Content-Type": "application/json",
453+
},
454+
body: JSON.stringify({ a: 1 }),
455+
});
456+
t.equal(response.statusCode, 200);
457+
t.equal(response.body, JSON.stringify({ a: 1 }));
458+
459+
// Cleanup servers
460+
server.close();
461+
server2.close();
462+
});
463+
464+
t.test("Body parsing in middleware", opts, async (t) => {
465+
const { Hono } = require("hono") as typeof import("hono");
466+
467+
const app = new Hono<{ Variables: { body: any } }>();
468+
469+
app.use(async (c, next) => {
470+
c.set("body", await c.req.json());
471+
return next();
472+
});
473+
474+
app.post("/", async (c) => {
475+
return c.json(getContext());
476+
});
477+
478+
const response = await app.request("/", {
479+
method: "POST",
480+
headers: {
481+
"Content-Type": "application/json",
482+
},
483+
body: JSON.stringify({ x: 42 }),
484+
});
485+
486+
const body = await response.json();
487+
t.match(body, {
488+
method: "POST",
489+
body: { x: 42 },
490+
source: "hono",
491+
route: "/",
492+
});
493+
});
494+
495+
t.test("invalid json body", opts, async (t) => {
496+
const app = getApp();
497+
498+
const response = await app.request("/json", {
499+
method: "POST",
500+
headers: {
501+
"Content-Type": "application/json",
502+
},
503+
body: "invalid",
504+
});
505+
506+
t.same(response.status, 400);
507+
t.same(await response.text(), "Invalid JSON");
508+
});

library/sources/hono/contextFromRequest.ts

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,27 @@
11
import type { Context as HonoContext } from "hono";
2-
import { Context } from "../../agent/Context";
2+
import { Context, getContext } from "../../agent/Context";
33
import { buildRouteFromURL } from "../../helpers/buildRouteFromURL";
44
import { getIPAddressFromRequest } from "../../helpers/getIPAddressFromRequest";
5-
import { isJsonContentType } from "../../helpers/isJsonContentType";
65
import { parse } from "../../helpers/parseCookies";
76
import { getRemoteAddress } from "./getRemoteAddress";
87

98
export async function contextFromRequest(c: HonoContext): Promise<Context> {
109
const { req } = c;
1110

12-
let body = undefined;
13-
const contentType = req.header("content-type");
14-
if (contentType) {
15-
if (isJsonContentType(contentType)) {
16-
try {
17-
body = await req.json();
18-
} catch {
19-
// Ignore
20-
}
21-
} else if (contentType.startsWith("application/x-www-form-urlencoded")) {
22-
try {
23-
body = await req.parseBody();
24-
} catch {
25-
// Ignore
26-
}
27-
} else if (
28-
contentType.includes("text/plain") ||
29-
contentType.includes("xml")
30-
) {
31-
try {
32-
body = await req.text();
33-
} catch {
34-
// Ignore
35-
}
36-
}
37-
}
38-
3911
const cookieHeader = req.header("cookie");
12+
const existingContext = getContext();
4013

4114
return {
4215
method: c.req.method,
4316
remoteAddress: getIPAddressFromRequest({
4417
headers: req.header(),
4518
remoteAddress: getRemoteAddress(c),
4619
}),
47-
body: body,
20+
// Pass the body from the existing context if it's already set, otherwise the body is set in wrapRequestBodyParsing
21+
body:
22+
existingContext && existingContext.source === "hono"
23+
? existingContext.body
24+
: undefined,
4825
url: req.url,
4926
headers: req.header(),
5027
routeParams: req.param(),
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Context } from "hono";
2+
import { getContext, updateContext } from "../../agent/Context";
3+
import { createWrappedFunction, isWrapped } from "../../helpers/wrap";
4+
5+
// Wrap the request body parsing functions to update the context with the parsed body, if any of the functions are called.
6+
export function wrapRequestBodyParsing(req: Context["req"]) {
7+
req.parseBody = wrapBodyParsingFunction(req.parseBody);
8+
req.json = wrapBodyParsingFunction(req.json);
9+
req.text = wrapBodyParsingFunction(req.text);
10+
}
11+
12+
function wrapBodyParsingFunction<T extends Function>(func: T) {
13+
if (isWrapped(func)) {
14+
return func;
15+
}
16+
17+
return createWrappedFunction(func, function parse(parser) {
18+
return async function wrap() {
19+
// @ts-expect-error No type for arguments
20+
// eslint-disable-next-line prefer-rest-params
21+
const returnValue = await parser.apply(this, arguments);
22+
23+
if (returnValue) {
24+
const context = getContext();
25+
if (context) {
26+
updateContext(context, "body", returnValue);
27+
}
28+
}
29+
30+
return returnValue;
31+
};
32+
}) as T;
33+
}

library/sources/hono/wrapRequestHandler.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Handler, MiddlewareHandler } from "hono";
22
import { runWithContext } from "../../agent/Context";
33
import { contextFromRequest } from "./contextFromRequest";
4+
import { wrapRequestBodyParsing } from "./wrapRequestBodyParsing";
45

56
export function wrapRequestHandler(
67
handler: Handler | MiddlewareHandler
@@ -9,6 +10,8 @@ export function wrapRequestHandler(
910
const context = await contextFromRequest(c);
1011

1112
return await runWithContext(context, async () => {
13+
wrapRequestBodyParsing(c.req);
14+
1215
return await handler(c, next);
1316
});
1417
};

0 commit comments

Comments
 (0)