Skip to content

Commit

Permalink
Fix duplicate slash handling (#7935)
Browse files Browse the repository at this point in the history
* fix(#7806): collapse duplicate slashes

* refactor: handle request.url with duplicate slashes

* chore: improve duplicate slash test

* fix: only collapse duplicate slashes once

* chore: appease TS
  • Loading branch information
natemoo-re authored Aug 3, 2023
1 parent 705432f commit 6035bb3
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/clean-tools-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/internal-helpers': patch
---

Add `collapseDuplicateSlashes` helper
5 changes: 5 additions & 0 deletions .changeset/flat-toes-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Properly handle routing when multiple slashes are present in the request by collapsing them to a single `/`
10 changes: 7 additions & 3 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { SinglePageBuiltModule } from '../build/types';
import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js';
import { consoleLogDestination } from '../logger/console.js';
import { error, type LogOptions } from '../logger/core.js';
import { prependForwardSlash, removeTrailingForwardSlash } from '../path.js';
import { prependForwardSlash, removeTrailingForwardSlash, collapseDuplicateSlashes } from '../path.js';
import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
import { isResponse } from '../render/core.js';
import {
Expand Down Expand Up @@ -126,13 +126,17 @@ export class App {
const url = new URL(request.url);
// ignore requests matching public assets
if (this.#manifest.assets.has(url.pathname)) return undefined;
let pathname = prependForwardSlash(this.removeBase(url.pathname));
let routeData = matchRoute(pathname, this.#manifestData);
const pathname = prependForwardSlash(this.removeBase(url.pathname));
const routeData = matchRoute(pathname, this.#manifestData);
// missing routes fall-through, prerendered are handled by static layer
if (!routeData || routeData.prerender) return undefined;
return routeData;
}
async render(request: Request, routeData?: RouteData, locals?: object): Promise<Response> {
// Handle requests with duplicate slashes gracefully by cloning with a cleaned-up request URL
if (request.url !== collapseDuplicateSlashes(request.url)) {
request = new Request(collapseDuplicateSlashes(request.url), request);
}
if (!routeData) {
routeData = this.match(request);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/vite-plugin-astro-server/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { collectErrorMetadata } from '../core/errors/dev/index.js';
import { createSafeError } from '../core/errors/index.js';
import { error } from '../core/logger/core.js';
import * as msg from '../core/messages.js';
import { removeTrailingForwardSlash } from '../core/path.js';
import { removeTrailingForwardSlash, collapseDuplicateSlashes } from '../core/path.js';
import { eventError, telemetry } from '../events/index.js';
import { isServerLikeOutput } from '../prerender/utils.js';
import { runWithErrorHandling } from './controller.js';
Expand Down Expand Up @@ -37,7 +37,7 @@ export async function handleRequest({
const origin = `${moduleLoader.isHttps() ? 'https' : 'http'}://${incomingRequest.headers.host}`;
const buildingToSSR = isServerLikeOutput(config);

const url = new URL(origin + incomingRequest.url);
const url = new URL(collapseDuplicateSlashes(origin + incomingRequest.url));
let pathname: string;
if (config.trailingSlash === 'never' && !incomingRequest.url) {
pathname = '';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
const origin = Astro.url.origin;
const requestPathname = new URL(Astro.request.url).pathname;
const pathname = Astro.url.pathname;
---

<html>
Expand All @@ -15,6 +17,8 @@ const origin = Astro.url.origin;
</script>
</head>
<body>
<h1 id="origin">{origin}</h1>
<p id="origin">{origin}</p>
<p id="pathname">{pathname}</p>
<p id="request-pathname">{requestPathname}</p>
</body>
</html>
12 changes: 12 additions & 0 deletions packages/astro/test/ssr-request.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ describe('Using Astro.request in SSR', () => {
expect($('#origin').text()).to.equal('http://example.com');
});

it('Duplicate slashes are collapsed', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/subpath////request/////');
const response = await app.render(request);
expect(response.status).to.equal(200);
const html = await response.text();
const $ = cheerioLoad(html);
expect($('#origin').text()).to.equal('http://example.com');
expect($('#pathname').text()).to.equal('/subpath/request/');
expect($('#request-pathname').text()).to.equal('/subpath/request/');
});

it('public file is copied over', async () => {
const json = await fixture.readFile('/client/cars.json');
expect(json).to.not.be.undefined;
Expand Down
4 changes: 4 additions & 0 deletions packages/internal-helpers/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export function prependForwardSlash(path: string) {
return path[0] === '/' ? path : '/' + path;
}

export function collapseDuplicateSlashes(path: string) {
return path.replace(/(?<!:)\/\/+/g, '/');
}

export function removeTrailingForwardSlash(path: string) {
return path.endsWith('/') ? path.slice(0, path.length - 1) : path;
}
Expand Down

0 comments on commit 6035bb3

Please sign in to comment.