Fixes Node adapter receiving a request body (#4023)
* Fixes Node adapter receiving a request body * Updated lockfile
This commit is contained in:
parent
9aecf7c7c7
commit
4ca6a0933d
9 changed files with 274 additions and 16 deletions
6
.changeset/smooth-seahorses-hear.md
Normal file
6
.changeset/smooth-seahorses-hear.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'astro': patch
|
||||
'@astrojs/node': patch
|
||||
---
|
||||
|
||||
Fixes Node adapter to accept a request body
|
|
@ -7,15 +7,16 @@ import { App } from './index.js';
|
|||
|
||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||
|
||||
function createRequestFromNodeRequest(req: IncomingMessage): Request {
|
||||
function createRequestFromNodeRequest(req: IncomingMessage, body?: string): Request {
|
||||
let url = `http://${req.headers.host}${req.url}`;
|
||||
let rawHeaders = req.headers as Record<string, any>;
|
||||
const entries = Object.entries(rawHeaders);
|
||||
let request = new Request(url, {
|
||||
method: req.method || 'GET',
|
||||
headers: new Headers(entries),
|
||||
body
|
||||
});
|
||||
if (req.socket.remoteAddress) {
|
||||
if (req.socket?.remoteAddress) {
|
||||
Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress);
|
||||
}
|
||||
return request;
|
||||
|
@ -26,6 +27,31 @@ export class NodeApp extends App {
|
|||
return super.match(req instanceof Request ? req : createRequestFromNodeRequest(req));
|
||||
}
|
||||
render(req: IncomingMessage | Request) {
|
||||
if('on' in req) {
|
||||
let body: string | undefined = undefined;
|
||||
let reqBodyComplete = new Promise((resolve, reject) => {
|
||||
req.on('data', d => {
|
||||
if(body === undefined) {
|
||||
body = '';
|
||||
}
|
||||
if(d instanceof Buffer) {
|
||||
body += d.toString('utf-8');
|
||||
} else if(typeof d === 'string') {
|
||||
body += d;
|
||||
}
|
||||
});
|
||||
req.on('end', () => {
|
||||
resolve(body);
|
||||
});
|
||||
req.on('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
return reqBodyComplete.then(() => {
|
||||
return super.render(req instanceof Request ? req : createRequestFromNodeRequest(req, body));
|
||||
});
|
||||
}
|
||||
return super.render(req instanceof Request ? req : createRequestFromNodeRequest(req));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,13 +24,15 @@
|
|||
"scripts": {
|
||||
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
|
||||
"build:ci": "astro-scripts build \"src/**/*.ts\"",
|
||||
"dev": "astro-scripts dev \"src/**/*.ts\""
|
||||
"dev": "astro-scripts dev \"src/**/*.ts\"",
|
||||
"test": "mocha --exit --timeout 20000 test/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/webapi": "^0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*"
|
||||
"astro-scripts": "workspace:*",
|
||||
"node-mocks-http": "^1.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,21 +12,28 @@ export function createExports(manifest: SSRManifest) {
|
|||
const app = new NodeApp(manifest);
|
||||
return {
|
||||
async handler(req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) {
|
||||
const route = app.match(req);
|
||||
try {
|
||||
const route = app.match(req);
|
||||
|
||||
if (route) {
|
||||
try {
|
||||
const response = await app.render(req);
|
||||
await writeWebResponse(res, response);
|
||||
} catch (err: unknown) {
|
||||
if (next) {
|
||||
next(err);
|
||||
} else {
|
||||
throw err;
|
||||
if (route) {
|
||||
try {
|
||||
const response = await app.render(req);
|
||||
await writeWebResponse(res, response);
|
||||
} catch (err: unknown) {
|
||||
if (next) {
|
||||
next(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else if (next) {
|
||||
return next();
|
||||
}
|
||||
} catch(err: unknown) {
|
||||
if(!res.headersSent) {
|
||||
res.writeHead(500, `Server error`);
|
||||
res.end();
|
||||
}
|
||||
} else if (next) {
|
||||
return next();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
37
packages/integrations/node/test/api-route.test.js
Normal file
37
packages/integrations/node/test/api-route.test.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import nodejs from '../dist/index.js';
|
||||
import { loadFixture, createRequestAndResponse, toPromise } from './test-utils.js';
|
||||
import { expect } from 'chai';
|
||||
|
||||
|
||||
describe('API routes', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/api-route/',
|
||||
experimental: {
|
||||
ssr: true,
|
||||
},
|
||||
adapter: nodejs(),
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Can get the request body', async () => {
|
||||
const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs');
|
||||
|
||||
let { req, res, done } = createRequestAndResponse({
|
||||
method: 'POST',
|
||||
url: '/recipes'
|
||||
});
|
||||
|
||||
handler(req, res);
|
||||
req.send(JSON.stringify({ id: 2 }));
|
||||
|
||||
let [ buffer ] = await done;
|
||||
let json = JSON.parse(buffer.toString('utf-8'));
|
||||
expect(json.length).to.equal(1);
|
||||
expect(json[0].name).to.equal('Broccoli Soup');
|
||||
});
|
||||
});
|
9
packages/integrations/node/test/fixtures/api-route/package.json
vendored
Normal file
9
packages/integrations/node/test/fixtures/api-route/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/nodejs-api-route",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
24
packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js
vendored
Normal file
24
packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
|
||||
export async function post({ request }) {
|
||||
let body = await request.json();
|
||||
const recipes = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Potato Soup'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Broccoli Soup'
|
||||
}
|
||||
];
|
||||
|
||||
let out = recipes.filter(r => {
|
||||
return r.id === body.id;
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(out), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
41
packages/integrations/node/test/test-utils.js
Normal file
41
packages/integrations/node/test/test-utils.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js';
|
||||
import httpMocks from 'node-mocks-http';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../astro/test/test-utils').Fixture} Fixture
|
||||
*/
|
||||
|
||||
export function loadFixture(inlineConfig) {
|
||||
if (!inlineConfig || !inlineConfig.root)
|
||||
throw new Error("Must provide { root: './fixtures/...' }");
|
||||
|
||||
// resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath
|
||||
// without this, the main `loadFixture` helper will resolve relative to `packages/astro/test`
|
||||
return baseLoadFixture({
|
||||
...inlineConfig,
|
||||
root: new URL(inlineConfig.root, import.meta.url).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
export function createRequestAndResponse(reqOptions) {
|
||||
let req = httpMocks.createRequest(reqOptions);
|
||||
|
||||
let res = httpMocks.createResponse({
|
||||
eventEmitter: EventEmitter,
|
||||
req
|
||||
});
|
||||
|
||||
let done = toPromise(res);
|
||||
|
||||
return { req, res, done };
|
||||
}
|
||||
|
||||
export function toPromise(res) {
|
||||
return new Promise(resolve => {
|
||||
res.on('end', () => {
|
||||
let chunks = res._getChunks();
|
||||
resolve(chunks);
|
||||
});
|
||||
});
|
||||
}
|
106
pnpm-lock.yaml
106
pnpm-lock.yaml
|
@ -2217,11 +2217,21 @@ importers:
|
|||
'@astrojs/webapi': ^0.12.0
|
||||
astro: workspace:*
|
||||
astro-scripts: workspace:*
|
||||
node-mocks-http: ^1.11.0
|
||||
dependencies:
|
||||
'@astrojs/webapi': link:../../webapi
|
||||
devDependencies:
|
||||
astro: link:../../astro
|
||||
astro-scripts: link:../../../scripts
|
||||
node-mocks-http: 1.11.0
|
||||
|
||||
packages/integrations/node/test/fixtures/api-route:
|
||||
specifiers:
|
||||
'@astrojs/node': workspace:*
|
||||
astro: workspace:*
|
||||
dependencies:
|
||||
'@astrojs/node': link:../../..
|
||||
astro: link:../../../../../astro
|
||||
|
||||
packages/integrations/partytown:
|
||||
specifiers:
|
||||
|
@ -8778,6 +8788,14 @@ packages:
|
|||
event-target-shim: 5.0.1
|
||||
dev: true
|
||||
|
||||
/accepts/1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-types: 2.1.35
|
||||
negotiator: 0.6.3
|
||||
dev: true
|
||||
|
||||
/acorn-jsx/5.3.2_acorn@8.8.0:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
|
@ -9577,6 +9595,13 @@ packages:
|
|||
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
||||
dev: false
|
||||
|
||||
/content-disposition/0.5.4:
|
||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/convert-source-map/1.8.0:
|
||||
resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==}
|
||||
dependencies:
|
||||
|
@ -9864,6 +9889,11 @@ packages:
|
|||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||
dev: false
|
||||
|
||||
/depd/1.1.2:
|
||||
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/depd/2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
@ -10757,6 +10787,11 @@ packages:
|
|||
/fraction.js/4.2.0:
|
||||
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
|
||||
|
||||
/fresh/0.5.2:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/fs-constants/1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
|
@ -12278,6 +12313,11 @@ packages:
|
|||
resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
|
||||
dev: false
|
||||
|
||||
/media-typer/0.3.0:
|
||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/meow/6.1.1:
|
||||
resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -12295,6 +12335,10 @@ packages:
|
|||
yargs-parser: 18.1.3
|
||||
dev: true
|
||||
|
||||
/merge-descriptors/1.0.1:
|
||||
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
|
||||
dev: true
|
||||
|
||||
/merge-stream/2.0.0:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
|
||||
|
@ -12302,6 +12346,11 @@ packages:
|
|||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
/methods/1.1.2:
|
||||
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/micromark-core-commonmark/1.0.6:
|
||||
resolution: {integrity: sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==}
|
||||
dependencies:
|
||||
|
@ -12652,6 +12701,24 @@ packages:
|
|||
resolution: {integrity: sha512-pDEgWjUoCMBwME8z8UiCOO6FKH0It1LASFh8hFSk8uSyfyw6rqY4PBk2LiIEPaVHwtLDhozp4Pr0I+yAUfCpiA==}
|
||||
dev: false
|
||||
|
||||
/mime-db/1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/mime-types/2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: true
|
||||
|
||||
/mime/1.6.0:
|
||||
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/mime/3.0.0:
|
||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
@ -12833,6 +12900,11 @@ packages:
|
|||
- supports-color
|
||||
dev: false
|
||||
|
||||
/negotiator/0.6.3:
|
||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/netmask/2.0.2:
|
||||
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
@ -12893,6 +12965,22 @@ packages:
|
|||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/node-mocks-http/1.11.0:
|
||||
resolution: {integrity: sha512-jS/WzSOcKbOeGrcgKbenZeNhxUNnP36Yw11+hL4TTxQXErGfqYZ+MaYNNvhaTiGIJlzNSqgQkk9j8dSu1YWSuw==}
|
||||
engines: {node: '>=0.6'}
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
content-disposition: 0.5.4
|
||||
depd: 1.1.2
|
||||
fresh: 0.5.2
|
||||
merge-descriptors: 1.0.1
|
||||
methods: 1.1.2
|
||||
mime: 1.6.0
|
||||
parseurl: 1.3.3
|
||||
range-parser: 1.2.1
|
||||
type-is: 1.6.18
|
||||
dev: true
|
||||
|
||||
/node-pre-gyp/0.13.0:
|
||||
resolution: {integrity: sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ==}
|
||||
deprecated: 'Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future'
|
||||
|
@ -13264,6 +13352,11 @@ packages:
|
|||
entities: 4.3.1
|
||||
dev: true
|
||||
|
||||
/parseurl/1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/pascal-case/3.1.2:
|
||||
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
|
||||
dependencies:
|
||||
|
@ -13938,6 +14031,11 @@ packages:
|
|||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/range-parser/1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/raw-body/2.5.1:
|
||||
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
@ -15503,6 +15601,14 @@ packages:
|
|||
engines: {node: '>=12.20'}
|
||||
dev: false
|
||||
|
||||
/type-is/1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
media-typer: 0.3.0
|
||||
mime-types: 2.1.35
|
||||
dev: true
|
||||
|
||||
/typescript/4.6.4:
|
||||
resolution: {integrity: sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==}
|
||||
engines: {node: '>=4.2.0'}
|
||||
|
|
Loading…
Reference in a new issue