Skip to content

Commit

Permalink
Add server.headers option (#5564)
Browse files Browse the repository at this point in the history
With this new `server.headers` option, the users can specify
custom headers for `astro dev` and `astro preview` servers.

This is useful when they want to build a website requiring
specific response headers such as `Cross-Origin-Opener-Policy`.
  • Loading branch information
riywo committed Dec 14, 2022
1 parent 8913c51 commit dced4a8
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-moose-attend.md
@@ -0,0 +1,5 @@
---
'astro': minor
---

Add `server.headers` option
3 changes: 3 additions & 0 deletions packages/astro/src/core/config/schema.ts
Expand Up @@ -9,6 +9,7 @@ import { fileURLToPath } from 'url';
import { z } from 'zod';
import { appendForwardSlash, prependForwardSlash, trimSlashes } from '../path.js';
import { isObject } from '../util.js';
import { OutgoingHttpHeaders } from 'http';

const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
root: '.',
Expand Down Expand Up @@ -125,6 +126,7 @@ export const AstroConfigSchema = z.object({
.optional()
.default(ASTRO_CONFIG_DEFAULTS.server.host),
port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port),
headers: z.custom<OutgoingHttpHeaders>().optional(),
})
.optional()
.default({})
Expand Down Expand Up @@ -287,6 +289,7 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
.optional()
.default(ASTRO_CONFIG_DEFAULTS.server.host),
port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port),
headers: z.custom<OutgoingHttpHeaders>().optional(),
streaming: z.boolean().optional().default(true),
})
.optional()
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/dev/container.ts
Expand Up @@ -65,7 +65,7 @@ export async function createContainer(params: CreateContainerParams = {}): Promi
logging,
isRestart,
});
const { host } = settings.config.server;
const { host, headers } = settings.config.server;

// The client entrypoint for renderers. Since these are imported dynamically
// we need to tell Vite to preoptimize them.
Expand All @@ -76,7 +76,7 @@ export async function createContainer(params: CreateContainerParams = {}): Promi
const viteConfig = await createVite(
{
mode: 'development',
server: { host },
server: { host, headers },
optimizeDeps: {
include: rendererClientEntries,
},
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/preview/index.ts
Expand Up @@ -24,10 +24,10 @@ export default async function preview(
});
await runHookConfigDone({ settings: settings, logging: logging });
const host = getResolvedHostForHttpServer(settings.config.server.host);
const { port } = settings.config.server;
const { port, headers } = settings.config.server;

if (settings.config.output === 'static') {
const server = await createStaticPreviewServer(settings, { logging, host, port });
const server = await createStaticPreviewServer(settings, { logging, host, port, headers });
return server;
}
if (!settings.adapter) {
Expand Down
19 changes: 17 additions & 2 deletions packages/astro/src/core/preview/static-preview-server.ts
Expand Up @@ -3,7 +3,7 @@ import type { AstroSettings } from '../../@types/astro';
import type { LogOptions } from '../logger/core';

import fs from 'fs';
import http from 'http';
import http, { OutgoingHttpHeaders } from 'http';
import { performance } from 'perf_hooks';
import sirv from 'sirv';
import { fileURLToPath } from 'url';
Expand All @@ -24,7 +24,17 @@ const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/;
/** The primary dev action */
export default async function createStaticPreviewServer(
settings: AstroSettings,
{ logging, host, port }: { logging: LogOptions; host: string | undefined; port: number }
{
logging,
host,
port,
headers,
}: {
logging: LogOptions;
host: string | undefined;
port: number;
headers: OutgoingHttpHeaders | undefined;
}
): Promise<PreviewServer> {
const startServerTime = performance.now();
const defaultOrigin = 'http://localhost';
Expand All @@ -35,6 +45,11 @@ export default async function createStaticPreviewServer(
dev: true,
etag: true,
maxAge: 0,
setHeaders: (res, pathname, stats) => {
for (const [name, value] of Object.entries(headers ?? {})) {
if (value) res.setHeader(name, value);
}
},
});
// Create the preview server, send static files out of the `dist/` directory.
const server = http.createServer((req, res) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/astro/src/vite-plugin-astro-server/route.ts
Expand Up @@ -144,6 +144,11 @@ export async function handleRoute(
clientAddress: buildingToSSR ? req.socket.remoteAddress : undefined,
});

// Set user specified headers to response object.
for (const [name, value] of Object.entries(config.server.headers ?? {})) {
if (value) res.setHeader(name, value);
}

// attempt to get static paths
// if this fails, we have a bad URL match!
const paramsAndPropsRes = await getParamsAndProps({
Expand Down
39 changes: 39 additions & 0 deletions packages/astro/test/astro-dev-headers.test.js
@@ -0,0 +1,39 @@
import { expect } from 'chai';
import { loadFixture } from './test-utils.js';

describe('Astro dev headers', () => {
let fixture;
let devServer;
const headers = {
'x-astro': 'test',
};

before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-dev-headers/',
server: {
headers,
},
});
await fixture.build();
devServer = await fixture.startDevServer();
});

after(async () => {
await devServer.stop();
});

describe('dev', () => {
it('returns custom headers for valid URLs', async () => {
const result = await fixture.fetch('/');
expect(result.status).to.equal(200);
expect(Object.fromEntries(result.headers)).to.include(headers);
});

it('does not return custom headers for invalid URLs', async () => {
const result = await fixture.fetch('/bad-url');
expect(result.status).to.equal(404);
expect(Object.fromEntries(result.headers)).not.to.include(headers);
});
});
});
40 changes: 40 additions & 0 deletions packages/astro/test/astro-preview-headers.test.js
@@ -0,0 +1,40 @@
import { expect } from 'chai';
import { loadFixture } from './test-utils.js';

describe('Astro preview headers', () => {
let fixture;
let previewServer;
const headers = {
astro: 'test',
};

before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-preview-headers/',
server: {
headers,
},
});
await fixture.build();
previewServer = await fixture.preview();
});

// important: close preview server (free up port and connection)
after(async () => {
await previewServer.stop();
});

describe('preview', () => {
it('returns custom headers for valid URLs', async () => {
const result = await fixture.fetch('/');
expect(result.status).to.equal(200);
expect(Object.fromEntries(result.headers)).to.include(headers);
});

it('does not return custom headers for invalid URLs', async () => {
const result = await fixture.fetch('/bad-url');
expect(result.status).to.equal(404);
expect(Object.fromEntries(result.headers)).not.to.include(headers);
});
});
});
8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/astro-dev-headers/package.json
@@ -0,0 +1,8 @@
{
"name": "@test/astro-dev-headers",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
@@ -0,0 +1,8 @@
<html>
<head>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>

@@ -0,0 +1,8 @@
{
"name": "@test/astro-preview-headers",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
@@ -0,0 +1,8 @@
<html>
<head>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>

12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit dced4a8

Please sign in to comment.