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

fix: Bare paths redirect to paths with a trailing slash in production #8593

Closed
wants to merge 14 commits into from
29 changes: 23 additions & 6 deletions packages/integrations/node/src/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,36 @@ function parsePathname(pathname: string, host: string | undefined, port: number)

export function createServer(
{ client, port, host, removeBase }: CreateServerOptions,
handler: http.RequestListener
handler: http.RequestListener,
trailingSlash: string
) {
const listener: http.RequestListener = (req, res) => {
if (req.url) {
let pathname: string | undefined = removeBase(req.url);
pathname = pathname[0] === '/' ? pathname : '/' + pathname;
const encodedURI = parsePathname(pathname, host, port);
let encodedURI = parsePathname(pathname, host, port);

if (!encodedURI) {
res.writeHead(400);
res.end('Bad request.');
return res;
}

const stream = send(req, encodedURI, {
let pathForSend = encodedURI;

if (trailingSlash === 'never') {
if (pathname.endsWith('/')) {
encodedURI = parsePathname(pathname.substring(0, pathname.length - 1), host, port);
if (!encodedURI) {
res.writeHead(400);
res.end('Bad request.');
return res;
}
}
pathForSend = encodedURI + '/';
}

const stream = send(req, pathForSend, {
root: fileURLToPath(client),
dotfiles: pathname.startsWith('/.well-known/') ? 'allow' : 'deny',
});
Expand All @@ -64,9 +79,11 @@ export function createServer(
location = req.url + '/';
}

res.statusCode = 301;
res.setHeader('Location', location);
res.end(location);
if (!pathForSend.endsWith('/')) {
res.statusCode = 301;
res.setHeader('Location', location);
res.end(location);
}
});
stream.on('file', () => {
forwardError = true;
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default function createIntegration(userOptions: UserOptions): AstroIntegr
server: config.build.server?.toString(),
host: config.server.host,
port: config.server.port,
trailingSlash: config.trailingSlash,
};
setAdapter(getAdapter(_options));

Expand Down
3 changes: 2 additions & 1 deletion packages/integrations/node/src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ const preview: CreatePreviewServer = async function ({
host,
removeBase,
},
handler
handler,
'ignore'
);
const address = getNetworkAddress('http', host, port);

Expand Down
3 changes: 2 additions & 1 deletion packages/integrations/node/src/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export default function startServer(app: NodeApp, options: Options) {
host,
removeBase: app.removeBase.bind(app),
},
handler
handler,
options.trailingSlash
);

const protocol = server.server instanceof https.Server ? 'https' : 'http';
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/node/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface Options extends UserOptions {
port: number;
server: string;
client: string;
trailingSlash: string;
}

export type RequestHandlerParams = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@test/nodejs-trailing-slash",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
---
<html>
<head>
<title>One</title>
</head>
<body>
<h1>One</h1>
</body>
</html>
82 changes: 82 additions & 0 deletions packages/integrations/node/test/trailing-slash.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import nodejs from '../dist/index.js';
import { loadFixture } from './test-utils.js';
import { expect } from 'chai';
/**
* @typedef {import('astro/test/test-utils.js').Fixture} Fixture
*/

async function load() {
const mod = await import(
`./fixtures/trailing-slash/dist/server/entry.mjs?dropcache=${Date.now()}`
);
return mod;
}

describe('Trailing Slash In Production', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;
let server;

describe('By default does add the trailing slash', async () => {
before(async () => {
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
process.env.PRERENDER = true;

fixture = await loadFixture({
// inconsequential config that differs between tests
// to bust cache and prevent modules and their state
// from being reused
output: 'hybrid',
root: './fixtures/trailing-slash/',
adapter: nodejs({ mode: 'standalone' }),
});
await fixture.build();
const { startServer } = await load();
let res = startServer();
server = res.server;
});

after(async () => {
await server.stop();
await fixture.clean();
delete process.env.PRERENDER;
});

it('redirects each route to have trailing slash', async () => {
const res = await fetch(`http://${server.host}:${server.port}/one`);
expect(res.url).to.contain('one/');
});
});

describe('Does not add trailing slash when set to never', async () => {
before(async () => {
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
process.env.PRERENDER = true;

fixture = await loadFixture({
// inconsequential config that differs between tests
// to bust cache and prevent modules and their state
// from being reused
output: 'hybrid',
trailingSlash: 'never',
root: './fixtures/trailing-slash/',
adapter: nodejs({ mode: 'standalone' }),
});
await fixture.build();
const { startServer } = await load();
let res = startServer();
server = res.server;
});

after(async () => {
await server.stop();
await fixture.clean();
delete process.env.PRERENDER;
});

it('redirects each route to have trailing slash', async () => {
const res = await fetch(`http://${server.host}:${server.port}/one`);
expect(res.url).to.not.contain('one/');
});
});
});
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

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