summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/smooth-seahorses-hear.md6
-rw-r--r--packages/astro/src/core/app/node.ts30
-rw-r--r--packages/integrations/node/package.json6
-rw-r--r--packages/integrations/node/src/server.ts31
-rw-r--r--packages/integrations/node/test/api-route.test.js37
-rw-r--r--packages/integrations/node/test/fixtures/api-route/package.json9
-rw-r--r--packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js24
-rw-r--r--packages/integrations/node/test/test-utils.js41
-rw-r--r--pnpm-lock.yaml106
9 files changed, 274 insertions, 16 deletions
diff --git a/.changeset/smooth-seahorses-hear.md b/.changeset/smooth-seahorses-hear.md
new file mode 100644
index 000000000..0c203dc10
--- /dev/null
+++ b/.changeset/smooth-seahorses-hear.md
@@ -0,0 +1,6 @@
+---
+'astro': patch
+'@astrojs/node': patch
+---
+
+Fixes Node adapter to accept a request body
diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts
index 7b6298809..11d93013e 100644
--- a/packages/astro/src/core/app/node.ts
+++ b/packages/astro/src/core/app/node.ts
@@ -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));
}
}
diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json
index 7886e93fc..d39d49124 100644
--- a/packages/integrations/node/package.json
+++ b/packages/integrations/node/package.json
@@ -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"
}
}
diff --git a/packages/integrations/node/src/server.ts b/packages/integrations/node/src/server.ts
index c07b5a91b..453ecb2d2 100644
--- a/packages/integrations/node/src/server.ts
+++ b/packages/integrations/node/src/server.ts
@@ -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();
}
},
};
diff --git a/packages/integrations/node/test/api-route.test.js b/packages/integrations/node/test/api-route.test.js
new file mode 100644
index 000000000..963e0463a
--- /dev/null
+++ b/packages/integrations/node/test/api-route.test.js
@@ -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');
+ });
+});
diff --git a/packages/integrations/node/test/fixtures/api-route/package.json b/packages/integrations/node/test/fixtures/api-route/package.json
new file mode 100644
index 000000000..c4d9bdd2b
--- /dev/null
+++ b/packages/integrations/node/test/fixtures/api-route/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/nodejs-api-route",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*",
+ "@astrojs/node": "workspace:*"
+ }
+}
diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js b/packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js
new file mode 100644
index 000000000..edbd15a0e
--- /dev/null
+++ b/packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js
@@ -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'
+ }
+ });
+}
diff --git a/packages/integrations/node/test/test-utils.js b/packages/integrations/node/test/test-utils.js
new file mode 100644
index 000000000..4bd42d557
--- /dev/null
+++ b/packages/integrations/node/test/test-utils.js
@@ -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);
+ });
+ });
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9b64e6599..b728d2139 100644
--- a/pnpm-lock.yaml
+++ b/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'}