From f955c4eb1b911c8a71131d6cd465e6d3f15f3ff0 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Fri, 25 Sep 2026 16:05:24 +0700 Subject: [PATCH] fix(node): ignore req.body that express 4's body-parser assigns without parsing body-parser 1.x (express 4) sets `req.body = {}` on every request, even the ones it leaves unread, so file, form-data, url-search-params, octet-stream and event-stream bodies all resolved to `{}`, and a bodyless request gave `{}` instead of `undefined`. Only trust `req.body` once the request stream has been consumed. Adds express 4 to the integration test matrix. --- package.json | 2 + packages/node/src/body.test.ts | 33 +++- packages/node/src/body.ts | 2 +- packages/node/src/types.ts | 1 + pnpm-lock.yaml | 292 +++++++++++++++++++++++++++++++ tests/client-server.expressjs.ts | 19 +- tests/data-transfer.test.ts | 2 + tests/signal-and-cancel.test.ts | 2 + 8 files changed, 347 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 377fe2b..3c71402 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@standard-server/peer": "workspace:*", "@standard-server/shared": "workspace:*", "@types/express": "^5.0.6", + "@types/express4": "npm:@types/express@^4.17.25", "@types/node": "^26.4.1", "@types/ws": "^8.18.1", "@vitest/coverage-v8": "^4.1.10", @@ -43,6 +44,7 @@ "eslint-plugin-ban": "^2.0.0", "eslint-plugin-format": "^2.0.1", "express": "^5.2.1", + "express4": "npm:express@^4.22.3", "fastify": "^5.11.0", "h3": "2.0.1-rc.26", "lint-staged": "^17.3.0", diff --git a/packages/node/src/body.test.ts b/packages/node/src/body.test.ts index d903f97..19b0894 100644 --- a/packages/node/src/body.test.ts +++ b/packages/node/src/body.test.ts @@ -4,6 +4,7 @@ import type { NodeHttpRequest } from './types' import { Buffer } from 'node:buffer' import http2 from 'node:http2' import { Readable } from 'node:stream' +import { text } from 'node:stream/consumers' import * as StandardServerModule from '@standard-server/core' import { toFetchHeaders } from '@standard-server/fetch' import { isAsyncIteratorObject } from '@standard-server/shared' @@ -151,7 +152,9 @@ describe('toStandardBody', () => { let standardBody: StandardBody = {} as any await request(async (req: IncomingMessage, res: ServerResponse) => { - // @ts-expect-error fake body is parsed + // fake an upstream parser: consume the stream, then assign the parsed body + await text(req) + // @ts-expect-error fake body is parsed req.body = { value: 123 } standardBody = await toStandardBody(req) res.end() @@ -162,6 +165,34 @@ describe('toStandardBody', () => { expect(standardBody).toEqual({ value: 123 }) }) + + // body-parser 1.x (express 4) assigns `{}` to every request, even the ones it leaves unread + it('ignore body assigned without consuming the stream', async () => { + let standardBody: any + + await request(async (req: IncomingMessage, res: ServerResponse) => { + // @ts-expect-error fake body is assigned + req.body = {} + standardBody = await toStandardBody(req) + res.end() + }) + .post('/') + .set('standard-server', 'file') + .send(Buffer.from('foo')) + + expect(standardBody).toBeInstanceOf(File) + expect(await standardBody.text()).toBe('foo') + + await request(async (req: IncomingMessage, res: ServerResponse) => { + // @ts-expect-error fake body is assigned + req.body = {} + standardBody = await toStandardBody(req) + res.end() + }) + .get('/') + + expect(standardBody).toBe(undefined) + }) }) describe('handle utf-8 characters split across stream chunks', () => { diff --git a/packages/node/src/body.ts b/packages/node/src/body.ts index 7fd16ca..55a7bac 100644 --- a/packages/node/src/body.ts +++ b/packages/node/src/body.ts @@ -23,7 +23,7 @@ export async function toStandardBody( options: ToStandardBodyOptions = {}, ): Promise { // body's already parsed by upstream framework like express, ... - if (req.body !== undefined) { + if (req.body !== undefined && !req.readable) { return req.body } diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 2b9c8f5..e71378c 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -9,6 +9,7 @@ export type NodeHttpRequest = (IncomingMessage | Http2ServerRequest) & { /** * Body might already parsed by upstream framework like express.js, ... + * Only used once the request stream has been consumed. */ body?: unknown } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 373f339..904afd5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,6 +199,9 @@ importers: '@types/express': specifier: ^5.0.6 version: 5.0.6 + '@types/express4': + specifier: npm:@types/express@^4.17.25 + version: '@types/express@4.17.25' '@types/node': specifier: ^26.4.1 version: 26.4.1 @@ -226,6 +229,9 @@ importers: express: specifier: ^5.2.1 version: 5.2.1(supports-color@7.2.0) + express4: + specifier: npm:express@^4.22.3 + version: express@4.22.3(supports-color@7.2.0) fastify: specifier: ^5.11.0 version: 5.12.1 @@ -1494,9 +1500,15 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/express-serve-static-core@4.19.9': + resolution: {integrity: sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==} + '@types/express-serve-static-core@5.1.3': resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} @@ -1518,6 +1530,9 @@ packages: '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -1533,9 +1548,15 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} @@ -1682,6 +1703,10 @@ packages: abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -1729,6 +1754,9 @@ packages: args-tokenizer@0.3.0: resolution: {integrity: sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q==} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -1776,6 +1804,10 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + body-parser@1.20.8: + resolution: {integrity: sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.3.0: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} @@ -1937,6 +1969,10 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -1959,6 +1995,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -2034,6 +2073,14 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2083,6 +2130,10 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-indent@7.0.2: resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==} engines: {node: '>=12.20'} @@ -2425,6 +2476,10 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + express@4.22.3: + resolution: {integrity: sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==} + engines: {node: '>= 0.10.0'} + express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} @@ -2500,6 +2555,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -2558,6 +2617,10 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -2673,6 +2736,10 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} @@ -2936,10 +3003,17 @@ packages: mdn-data@2.34.0: resolution: {integrity: sha512-OgIlLv0NxJKVW4GTSAoEgpRGd4F2XCqGinK0MsMlBCCS/Zcm2/LsbercNWNA7PeMMcjl75NnI97eqyo7zkdxWA==} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + media-typer@1.1.1: resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} engines: {node: '>= 0.8'} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -3054,6 +3128,11 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} engines: {node: '>=4.0.0'} @@ -3111,6 +3190,9 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3126,6 +3208,10 @@ packages: resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} engines: {node: '>=18'} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + negotiator@1.1.0: resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} engines: {node: '>=18'} @@ -3253,6 +3339,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} @@ -3548,10 +3637,18 @@ packages: resolution: {integrity: sha512-Y3NoRtprEEZQD8RfxMCfS0ZTqc4e+i18OrXEXAvpM6TfC/3y+0L5rNbZiSnbBBEkDfFzbpd8o+cE8q3/anjMGA==} engines: {node: '>=22'} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + range-parser@1.3.0: resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} engines: {node: '>= 0.6'} + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} @@ -3650,6 +3747,9 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex2@5.1.1: resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} hasBin: true @@ -3684,10 +3784,18 @@ packages: engines: {node: '>=10'} hasBin: true + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -3953,6 +4061,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + type-is@2.1.0: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} @@ -4018,6 +4130,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -5002,6 +5118,13 @@ snapshots: '@types/estree@1.0.9': {} + '@types/express-serve-static-core@4.19.9': + dependencies: + '@types/node': 26.4.1 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + '@types/express-serve-static-core@5.1.3': dependencies: '@types/node': 26.4.1 @@ -5009,6 +5132,13 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 1.2.1 + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.9 + '@types/qs': 6.15.1 + '@types/serve-static': 1.15.10 + '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 @@ -5031,6 +5161,8 @@ snapshots: '@types/methods@1.1.4': {} + '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} '@types/node@26.4.1': @@ -5043,10 +5175,21 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 26.4.1 + '@types/send@1.2.1': dependencies: '@types/node': 26.4.1 + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 26.4.1 + '@types/send': 0.17.6 + '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 @@ -5262,6 +5405,11 @@ snapshots: abstract-logging@2.0.1: {} + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -5308,6 +5456,8 @@ snapshots: args-tokenizer@0.3.0: {} + array-flatten@1.1.1: {} + asap@2.0.6: {} asn1js@3.0.10: @@ -5358,6 +5508,23 @@ snapshots: binary-extensions@2.3.0: {} + body-parser@1.20.8(supports-color@7.2.0): + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9(supports-color@7.2.0) + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.16.0 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + body-parser@2.3.0(supports-color@7.2.0): dependencies: bytes: 3.1.2 @@ -5558,6 +5725,10 @@ snapshots: consola@3.4.2: {} + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + content-disposition@1.1.0: {} content-type@1.0.5: {} @@ -5570,6 +5741,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -5664,6 +5837,12 @@ snapshots: dependencies: css-tree: 2.2.1 + debug@2.6.9(supports-color@7.2.0): + dependencies: + ms: 2.0.0 + optionalDependencies: + supports-color: 7.2.0 + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 @@ -5697,6 +5876,8 @@ snapshots: destr@2.0.5: {} + destroy@1.2.0: {} + detect-indent@7.0.2: {} devlop@1.1.0: @@ -6153,6 +6334,42 @@ snapshots: expect-type@1.4.0: {} + express@4.22.3(supports-color@7.2.0): + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.8(supports-color@7.2.0) + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9(supports-color@7.2.0) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2(supports-color@7.2.0) + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.7 + qs: 6.16.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2(supports-color@7.2.0) + serve-static: 1.16.3(supports-color@7.2.0) + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + express@5.2.1(supports-color@7.2.0): dependencies: accepts: 2.0.0 @@ -6267,6 +6484,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@1.3.2(supports-color@7.2.0): + dependencies: + debug: 2.6.9(supports-color@7.2.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + finalhandler@2.1.1(supports-color@7.2.0): dependencies: debug: 4.4.3(supports-color@7.2.0) @@ -6333,6 +6562,8 @@ snapshots: fraction.js@5.3.4: {} + fresh@0.5.2: {} + fresh@2.0.0: {} fs-minipass@2.1.0: @@ -6440,6 +6671,10 @@ snapshots: transitivePeerDependencies: - supports-color + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -6763,8 +6998,12 @@ snapshots: mdn-data@2.34.0: {} + media-typer@0.3.0: {} + media-typer@1.1.1: {} + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} methods@1.1.2: {} @@ -6989,6 +7228,8 @@ snapshots: dependencies: mime-db: 1.54.0 + mime@1.6.0: {} + mime@2.6.0: {} minimatch@10.2.6: @@ -7037,6 +7278,8 @@ snapshots: mri@1.2.0: {} + ms@2.0.0: {} + ms@2.1.3: {} nanoid@3.3.18: {} @@ -7045,6 +7288,8 @@ snapshots: natural-orderby@5.0.0: {} + negotiator@0.6.3: {} + negotiator@1.1.0: dependencies: content-type: 2.1.0 @@ -7178,6 +7423,8 @@ snapshots: path-parse@1.0.7: {} + path-to-regexp@0.1.13: {} + path-to-regexp@8.4.2: {} pathe@1.1.2: {} @@ -7454,8 +7701,17 @@ snapshots: quote-js-string@0.1.0: {} + range-parser@1.2.1: {} + range-parser@1.3.0: {} + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -7576,6 +7832,8 @@ snapshots: run-applescript@7.1.0: {} + safe-buffer@5.2.1: {} + safe-regex2@5.1.1: dependencies: ret: 0.5.0 @@ -7603,6 +7861,24 @@ snapshots: semver@7.8.5: {} + send@0.19.2(supports-color@7.2.0): + dependencies: + debug: 2.6.9(supports-color@7.2.0) + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + send@1.2.1(supports-color@7.2.0): dependencies: debug: 4.4.3(supports-color@7.2.0) @@ -7619,6 +7895,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@1.16.3(supports-color@7.2.0): + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + serve-static@2.2.1(supports-color@7.2.0): dependencies: encodeurl: 2.0.0 @@ -7869,6 +8154,11 @@ snapshots: type-fest@4.41.0: {} + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + type-is@2.1.0: dependencies: content-type: 2.1.0 @@ -7975,6 +8265,8 @@ snapshots: util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} + vary@1.1.2: {} verkit@0.3.2: {} diff --git a/tests/client-server.expressjs.ts b/tests/client-server.expressjs.ts index 5f18b9d..82da155 100644 --- a/tests/client-server.expressjs.ts +++ b/tests/client-server.expressjs.ts @@ -3,8 +3,16 @@ import type { ClientServerTest } from './client-server' import { toFetchBody, toFetchHeaders, toStandardLazyResponse } from '@standard-server/fetch' import { sendStandardResponse, toStandardLazyRequest } from '@standard-server/node' import express from 'express' +import express4 from 'express4' export interface ExpressjsClientServerTestOptions { + /** + * Major version of express to run, each ships a different body-parser major. + * + * @default 5 + */ + version?: 4 | 5 + /** * Registers the body parsers a regular express app ships with, exercising the * `req.body` short-circuit in `toStandardBody` instead of reading the raw stream. @@ -19,15 +27,18 @@ export function createExpressjsClientServerTest( return { status: 404, body: 'Not Found', headers: {} } }) - const app = express() + // the api used below is the same across both majors, only their types are incompatible + const expressjs = (options.version === 4 ? express4 : express) as typeof express + const app = expressjs() if (options.bodyParser) { - // the parsers a regular express project registers; body-parser leaves `req.body` - // undefined for every other content type, so those bodies still reach the adapter as a stream + // the parsers a regular express project registers; for every other content type body-parser + // leaves the stream unread, so those bodies still reach the adapter as a stream. body-parser 1.x + // (express 4) still assigns `{}` to `req.body` then, while 2.x (express 5) leaves it undefined // // `strict` must be off: it defaults to on, which rejects every top-level JSON value that // isn't an object or array (`"a string"`, `null`, `1`) with a 400 before the adapter runs - app.use(express.json({ strict: false })) + app.use(expressjs.json({ strict: false })) } app.use(async (req, res) => { diff --git a/tests/data-transfer.test.ts b/tests/data-transfer.test.ts index 5ac313e..f44f25f 100644 --- a/tests/data-transfer.test.ts +++ b/tests/data-transfer.test.ts @@ -26,6 +26,8 @@ const PARALLEL_THRESHOLD = 150 describe.each([ ['expressjs', () => createExpressjsClientServerTest()], ['expressjs-body-parser', () => createExpressjsClientServerTest({ bodyParser: true })], + ['expressjs4', () => createExpressjsClientServerTest({ version: 4 })], + ['expressjs4-body-parser', () => createExpressjsClientServerTest({ version: 4, bodyParser: true })], ['inprogress', createInprogressClientServerTest], ['inprogress-fetch', createInprogressFetchClientServerTest], // ['h3-node-handler', createH3NodeHandlerClientServerTest], diff --git a/tests/signal-and-cancel.test.ts b/tests/signal-and-cancel.test.ts index dedb7ac..48fcc2d 100644 --- a/tests/signal-and-cancel.test.ts +++ b/tests/signal-and-cancel.test.ts @@ -34,6 +34,8 @@ function waitFor(assertion: () => T): Promise { describe.each([ ['expressjs', () => createExpressjsClientServerTest()], ['expressjs-body-parser', () => createExpressjsClientServerTest({ bodyParser: true })], + ['expressjs4', () => createExpressjsClientServerTest({ version: 4 })], + ['expressjs4-body-parser', () => createExpressjsClientServerTest({ version: 4, bodyParser: true })], // ['inprogress', createInprogressClientServerTest], // ['inprogress-fetch', createInprogressFetchClientServerTest], // ['h3-node-handler', createH3NodeHandlerClientServerTest],