diff --git a/.changeset/polite-melons-pump.md b/.changeset/polite-melons-pump.md new file mode 100644 index 000000000..db34f8408 --- /dev/null +++ b/.changeset/polite-melons-pump.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Allow file uploads in dev server diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts index 5d46d463c..05e82a226 100644 --- a/packages/astro/src/vite-plugin-astro-server/index.ts +++ b/packages/astro/src/vite-plugin-astro-server/index.ts @@ -215,13 +215,14 @@ async function handleRequest( let body: ArrayBuffer | undefined = undefined; if (!(req.method === 'GET' || req.method === 'HEAD')) { - let bytes: string[] = []; + let bytes: Uint8Array[] = []; await new Promise((resolve) => { - req.setEncoding('utf-8'); - req.on('data', (bts) => bytes.push(bts)); + req.on('data', part => { + bytes.push(part); + }); req.on('end', resolve); }); - body = new TextEncoder().encode(bytes.join('')).buffer; + body = Buffer.concat(bytes); } // Headers are only available when using SSR. diff --git a/packages/astro/test/api-routes.test.js b/packages/astro/test/api-routes.test.js index 80fb0970d..15d79d7f7 100644 --- a/packages/astro/test/api-routes.test.js +++ b/packages/astro/test/api-routes.test.js @@ -1,8 +1,11 @@ import { expect } from 'chai'; import * as cheerio from 'cheerio'; import { loadFixture } from './test-utils.js'; +import * as fs from 'fs'; +import { FormData, File } from 'node-fetch' describe('API routes', () => { + /** @type {import('./test-utils').Fixture} */ let fixture; before(async () => { diff --git a/packages/astro/test/fixtures/api-routes/package.json b/packages/astro/test/fixtures/api-routes/package.json index 0f7052df4..aa8c0adee 100644 --- a/packages/astro/test/fixtures/api-routes/package.json +++ b/packages/astro/test/fixtures/api-routes/package.json @@ -4,5 +4,8 @@ "private": true, "dependencies": { "astro": "workspace:*" + }, + "scripts": { + "dev": "astro dev" } } diff --git a/packages/astro/test/fixtures/ssr-api-route/src/images/penguin.jpg b/packages/astro/test/fixtures/ssr-api-route/src/images/penguin.jpg new file mode 100644 index 000000000..ae4daa548 Binary files /dev/null and b/packages/astro/test/fixtures/ssr-api-route/src/images/penguin.jpg differ diff --git a/packages/astro/test/fixtures/ssr-api-route/src/pages/binary.js b/packages/astro/test/fixtures/ssr-api-route/src/pages/binary.js new file mode 100644 index 000000000..3e1c70c81 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-api-route/src/pages/binary.js @@ -0,0 +1,32 @@ +import fs from 'node:fs'; + +export function get() { + return { + body: 'ok' + }; +} + +export async function post({ request }) { + const data = await request.formData(); + const file = data.get('file'); + + if (file) { + const buffer = await file.arrayBuffer(); + const realBuffer = await fs.promises.readFile(new URL('../images/penguin.jpg', import.meta.url)); + + if(buffersEqual(buffer, realBuffer)) { + return new Response('ok', { status: 200 }); + } + } + return new Response(null, { status: 400 }); +} + +function buffersEqual(buf1, buf2) { + if (buf1.byteLength != buf2.byteLength) return false; + const dv1 = new Uint8Array(buf1); + const dv2 = new Uint8Array(buf2); + for (let i = 0; i !== buf1.byteLength; i++) { + if (dv1[i] != dv2[i]) return false; + } + return true; +} diff --git a/packages/astro/test/ssr-api-route.test.js b/packages/astro/test/ssr-api-route.test.js index 8b007391d..5394ead51 100644 --- a/packages/astro/test/ssr-api-route.test.js +++ b/packages/astro/test/ssr-api-route.test.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { loadFixture } from './test-utils.js'; import testAdapter from './test-adapter.js'; +import { FormData, File } from 'node-fetch'; describe('API routes in SSR', () => { /** @type {import('./test-utils').Fixture} */ @@ -54,6 +55,20 @@ describe('API routes in SSR', () => { expect(text).to.equal(`ok`); }); + it('Can be passed binary data from multipart formdata', async () => { + const formData = new FormData(); + const raw = await fs.promises.readFile(new URL('./fixtures/ssr-api-route/src/images/penguin.jpg', import.meta.url)); + const file = new File([raw], 'penguin.jpg', { type: 'text/jpg' }); + formData.set('file', file, 'penguin.jpg'); + + const res = await fixture.fetch('/binary', { + method: 'POST', + body: formData + }); + + expect(res.status).to.equal(200); + }); + it('Infer content type with charset for { body } shorthand', async () => { const response = await fixture.fetch('/food.json', { method: 'GET', diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 59a925314..e1e4f73e7 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -27,7 +27,7 @@ polyfill(globalThis, { * @typedef {Object} Fixture * @property {typeof build} build * @property {(url: string) => string} resolveUrl - * @property {(url: string, opts: any) => Promise} fetch + * @property {(url: string, opts: Parameters[1]) => Promise} fetch * @property {(path: string) => Promise} readFile * @property {(path: string, updater: (content: string) => string) => Promise} writeFile * @property {(path: string) => Promise} readdir