Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support base64 encoding in Netlify Functions #3592

Merged
merged 5 commits into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hot-pumas-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/netlify': patch
---

Adds support for base64 encoded responses in Netlify Functions
24 changes: 24 additions & 0 deletions packages/integrations/netlify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,27 @@ And then point to the dist in your `netlify.toml`:
[functions]
directory = "dist/functions"
```

### binaryMediaTypes

> This option is only needed for the Functions adapter and is not needed for Edge Functions.

Netlify Functions sending binary data in the `body` need to be base64 encoded. The `@astrojs/netlify/functions` adapter handles this automatically based on the `Content-Type` header.

We check for common mime types for audio, image, and video files. To include specific mime types that should be treated as binary data, include the `binaryMediaTypes` option with a list of binary mime types.

```js
import fs from 'node:fs';

export function get() {
const buffer = fs.readFileSync('../image.jpg');

// Return the buffer directly, @astrojs/netlify will base64 encode the body
return new Response(buffer, {
status: 200,
headers: {
'content-type': 'image/jpeg'
}
});
}
```
10 changes: 6 additions & 4 deletions packages/integrations/netlify/src/integration-functions.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
import { createRedirects } from './shared.js';
import type { Args } from './netlify-functions.js';

export function getAdapter(): AstroAdapter {
export function getAdapter(args: Args = {}): AstroAdapter {
return {
name: '@astrojs/netlify/functions',
serverEntrypoint: '@astrojs/netlify/netlify-functions.js',
exports: ['handler'],
args: {},
args,
};
}

interface NetlifyFunctionsOptions {
dist?: URL;
binaryMediaTypes?: string[];
}

function netlifyFunctions({ dist }: NetlifyFunctionsOptions = {}): AstroIntegration {
function netlifyFunctions({ dist, binaryMediaTypes }: NetlifyFunctionsOptions = {}): AstroIntegration {
let _config: AstroConfig;
let entryFile: string;
return {
Expand All @@ -28,7 +30,7 @@ function netlifyFunctions({ dist }: NetlifyFunctionsOptions = {}): AstroIntegrat
}
},
'astro:config:done': ({ config, setAdapter }) => {
setAdapter(getAdapter());
setAdapter(getAdapter({ binaryMediaTypes }));
_config = config;
},
'astro:build:start': async ({ buildConfig }) => {
Expand Down
51 changes: 48 additions & 3 deletions packages/integrations/netlify/src/netlify-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,49 @@ polyfill(globalThis, {
exclude: 'window document',
});

interface Args {}
export interface Args {
binaryMediaTypes?: string[];
}

function parseContentType(header?: string) {
return header?.split(';')[0] ?? '';
}

export const createExports = (manifest: SSRManifest, args: Args) => {
const app = new App(manifest);

const binaryMediaTypes = args.binaryMediaTypes ?? [];
const knownBinaryMediaTypes = new Set([
'audio/3gpp',
'audio/3gpp2',
'audio/aac',
'audio/midi',
'audio/mpeg',
'audio/ogg',
'audio/opus',
'audio/wav',
'audio/webm',
'audio/x-midi',
'image/avif',
'image/bmp',
'image/gif',
'image/vnd.microsoft.icon',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/tiff',
'image/webp',
'video/3gpp',
'video/3gpp2',
'video/mp2t',
'video/mp4',
'video/mpeg',
'video/ogg',
'video/x-msvideo',
'video/webm',
...binaryMediaTypes,
]);

const handler: Handler = async (event) => {
const { httpMethod, headers, rawUrl, body: requestBody, isBase64Encoded } = event;
const init: RequestInit = {
Expand All @@ -34,13 +72,20 @@ export const createExports = (manifest: SSRManifest, args: Args) => {
}

const response: Response = await app.render(request);
const responseBody = await response.text();

const responseHeaders = Object.fromEntries(response.headers.entries());

const responseContentType = parseContentType(responseHeaders['content-type']);
const responseIsBase64Encoded = knownBinaryMediaTypes.has(responseContentType);

const responseBody = responseIsBase64Encoded
? Buffer.from(await response.text(), 'binary').toString('base64')
: await response.text();

const fnResponse: any = {
statusCode: response.status,
headers: responseHeaders,
body: responseBody,
isBase64Encoded: responseIsBase64Encoded,
};

// Special-case set-cookie which has to be set an different way :/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { expect } from 'chai';
import { loadFixture, testIntegration } from './test-utils.js';
import netlifyAdapter from '../../dist/index.js';

describe('Base64 Responses', () => {
/** @type {import('../../../astro/test/test-utils').Fixture} */
let fixture;

before(async () => {
fixture = await loadFixture({
root: new URL('./fixtures/base64-response/', import.meta.url).toString(),
experimental: {
ssr: true,
},
adapter: netlifyAdapter({
dist: new URL('./fixtures/base64-response/dist/', import.meta.url),
binaryMediaTypes: ['font/otf']
}),
site: `http://example.com`,
integrations: [testIntegration()],
});
await fixture.build();
});

it('Can return base64 encoded strings', async () => {
const entryURL = new URL(
'./fixtures/base64-response/.netlify/functions-internal/entry.mjs',
import.meta.url
);
const { handler } = await import(entryURL);
const resp = await handler({
httpMethod: 'GET',
headers: {},
rawUrl: 'http://example.com/image',
body: '{}',
isBase64Encoded: false,
});
expect(resp.statusCode, 'successful response').to.equal(200);
expect(resp.isBase64Encoded, 'includes isBase64Encoded flag').to.be.true;

const buffer = Buffer.from(resp.body, 'base64');
expect(buffer.toString(), 'decoded base64 string matches').to.equal('base64 test string');
});

it('Can define custom binaryMediaTypes', async () => {
const entryURL = new URL(
'./fixtures/base64-response/.netlify/functions-internal/entry.mjs',
import.meta.url
);
const { handler } = await import(entryURL);
const resp = await handler({
httpMethod: 'GET',
headers: {},
rawUrl: 'http://example.com/font',
body: '{}',
isBase64Encoded: false,
});
expect(resp.statusCode, 'successful response').to.equal(200);
expect(resp.isBase64Encoded, 'includes isBase64Encoded flag').to.be.true;

const buffer = Buffer.from(resp.body, 'base64');
expect(buffer.toString(), 'decoded base64 string matches').to.equal('base64 test font');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

export function get() {
const buffer = Buffer.from('base64 test font', 'utf-8')

return new Response(buffer, {
status: 200,
headers: {
'Content-Type': 'font/otf'
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

export function get() {
const buffer = Buffer.from('base64 test string', 'utf-8')

return new Response(buffer, {
status: 200,
headers: {
'content-type': 'image/jpeg;foo=foo'
}
});
}