Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,15 @@ describe('createServeRequestHandler', () => {
/missing.html 301 /missing → 404 404.html
/file?query=1 200 file.html
/file/?query=1 200 file.html
/file.html?query=1 301 /file → 200 file.html
/file.html?query=1 301 /file?query=1 → 200 file.html
/folder?query=1 200 folder/index.html
/folder?v=1.0 200 folder/index.html
/with%20space 200 with space.html
/with%20space.html 301 /with%20space → 200 with space.html
/файл 200 файл.html
/файл.html 301 /%D1%84%D0%B0%D0%B9%D0%BB → 200 файл.html
/café 200 café.html
//file 200 file.html
//file 301 /file → 200 file.html
/%E0%A4%A 400 Bad Request
/..%2Fdocusaurus.config.js 400 Bad Request"
`);
Expand All @@ -170,11 +170,11 @@ describe('createServeRequestHandler', () => {
/both/ 200 both/index.html
/both.html 301 /both → 301 /both/ → 200 both/index.html
/both/index.html 301 /both/index → 301 /both → 301 /both/ → 200 both/index.html
/docs/2.x 200 docs/2.x/index.html
/docs/2.x 301 /docs/2.x/ → 200 docs/2.x/index.html
/docs/2.x/ 200 docs/2.x/index.html
/docs/lodash.debounce 200 docs/lodash.debounce/index.html
/docs/lodash.debounce 301 /docs/lodash.debounce/ → 200 docs/lodash.debounce/index.html
/docs/lodash.debounce/ 200 docs/lodash.debounce/index.html
/docs/1.x 200 docs/1.x.html
/docs/1.x 301 /docs/1.x/ → 200 docs/1.x.html
/docs/1.x/ 200 docs/1.x.html
/assets/css/styles.css 200 assets/css/styles.css
/assets/css/styles.css?v=1 200 assets/css/styles.css
Expand All @@ -187,18 +187,18 @@ describe('createServeRequestHandler', () => {
/missing 301 /missing/ → 404 404.html
/missing/ 404 404.html
/missing.html 301 /missing → 301 /missing/ → 404 404.html
/file?query=1 301 /file/ → 200 file.html
/file?query=1 301 /file/?query=1 → 200 file.html
/file/?query=1 200 file.html
/file.html?query=1 301 /file → 301 /file/ → 200 file.html
/folder?query=1 301 /folder/ → 200 folder/index.html
/folder?v=1.0 301 /folder/ → 200 folder/index.html
/file.html?query=1 301 /file?query=1 → 301 /file/?query=1 → 200 file.html
/folder?query=1 301 /folder/?query=1 → 200 folder/index.html
/folder?v=1.0 301 /folder/?v=1.0 → 200 folder/index.html
/with%20space 301 /with%20space/ → 200 with space.html
/with%20space.html 301 /with%20space → 301 /with%20space/ → 200 with space.html
/файл 301 /%D1%84%D0%B0%D0%B9%D0%BB/ → 200 файл.html
/файл.html 301 /%D1%84%D0%B0%D0%B9%D0%BB → 301 /%D1%84%D0%B0%D0%B9%D0%BB/ → 200 файл.html
/café 301 /caf%C3%A9/ → 200 café.html
//file 301 /file → 301 /file/ → 200 file.html
/%E0%A4%A 400 Bad Request
/%E0%A4%A 301 /%E0%A4%A/ → 400 Bad Request
/..%2Fdocusaurus.config.js 400 Bad Request"
`);
});
Expand Down Expand Up @@ -241,8 +241,8 @@ describe('createServeRequestHandler', () => {
/missing/ 301 /missing → 404 404.html
/missing.html 301 /missing → 404 404.html
/file?query=1 200 file.html
/file/?query=1 301 /file → 200 file.html
/file.html?query=1 301 /file → 200 file.html
/file/?query=1 301 /file?query=1 → 200 file.html
/file.html?query=1 301 /file?query=1 → 200 file.html
/folder?query=1 200 folder/index.html
/folder?v=1.0 200 folder/index.html
/with%20space 200 with space.html
Expand Down Expand Up @@ -583,7 +583,6 @@ describe('createServeRequestHandler', () => {
});
});

// Without a baseUrl, serve-handler handles these requests
describe('without a baseUrl', () => {
it('trailingSlash undefined', async () => {
const result = await describeServing(
Expand All @@ -594,9 +593,9 @@ describe('createServeRequestHandler', () => {
expect(result).toMatchInlineSnapshot(`
"/..//example.com/index 400 Bad Request
/..//example.com/file.html 400 Bad Request
//example.com/index 301 /example.com → 404 404.html
//example.com/file 404 404.html
//example.com/file.html 301 /example.com/file → 404 404.html"
//example.com/index 301 /example.com/index → 301 /example.com → 404 404.html
//example.com/file 301 /example.com/file → 404 404.html
//example.com/file.html 301 /example.com/file.html → 301 /example.com/file → 404 404.html"
`);
});

Expand All @@ -609,9 +608,9 @@ describe('createServeRequestHandler', () => {
expect(result).toMatchInlineSnapshot(`
"/..//example.com/index 400 Bad Request
/..//example.com/file.html 400 Bad Request
//example.com/index 301 /example.com → 404 404.html
//example.com/index 301 /example.com/index → 301 /example.com → 404 404.html
//example.com/file 301 /example.com/file → 301 /example.com/file/ → 404 404.html
//example.com/file.html 301 /example.com/file → 301 /example.com/file/ → 404 404.html"
//example.com/file.html 301 /example.com/file.html → 301 /example.com/file → 301 /example.com/file/ → 404 404.html"
`);
});

Expand All @@ -624,9 +623,9 @@ describe('createServeRequestHandler', () => {
expect(result).toMatchInlineSnapshot(`
"/..//example.com/index 400 Bad Request
/..//example.com/file.html 400 Bad Request
//example.com/index 301 /example.com → 404 404.html
//example.com/index 301 /example.com/index → 301 /example.com → 404 404.html
//example.com/file 301 /example.com/file → 404 404.html
//example.com/file.html 301 /example.com/file → 404 404.html"
//example.com/file.html 301 /example.com/file.html → 301 /example.com/file → 404 404.html"
`);
});
});
Expand All @@ -649,14 +648,13 @@ describe('createServeRequestHandler', () => {
).resolves.toBe('301 /file → 302 /base/ → 200 index.html');
});

// TODO fix: keep the query string, "301 /file?query=1 → 200 file.html"
it('drops the query string in serve-handler redirects', async () => {
it('keeps the query string in redirects without a baseUrl', async () => {
await expect(
serveRequest(
{baseUrl: '/', trailingSlash: undefined},
'/file.html?query=1',
),
).resolves.toBe('301 /file → 200 file.html');
).resolves.toBe('301 /file?query=1 → 200 file.html');
});

// See https://github.com/vercel/serve-handler/pull/230
Expand Down Expand Up @@ -695,11 +693,10 @@ describe('createServeRequestHandler', () => {
).resolves.toBe('400 Bad Request');
});

// TODO fix: add it, "301 /docs/2.x/ → 200 docs/2.x/index.html"
it('adds no trailing slash to pages with a dot in their name, without a baseUrl', async () => {
it('adds a trailing slash to pages with a dot in their name, without a baseUrl', async () => {
await expect(
serveRequest({baseUrl: '/', trailingSlash: true}, '/docs/2.x'),
).resolves.toBe('200 docs/2.x/index.html');
).resolves.toBe('301 /docs/2.x/ → 200 docs/2.x/index.html');
});
});

Expand All @@ -718,7 +715,7 @@ describe('createServeRequestHandler', () => {
'/assets/',
'/assets/missing.js',
'/missing/',
'/%E0%A4%A',
'/%E0%A4%A/',
],
);
expect(result).toMatchInlineSnapshot(`
Expand All @@ -732,7 +729,7 @@ describe('createServeRequestHandler', () => {
/assets/ 404 no-cache + ETag
/assets/missing.js 404 no-cache + ETag
/missing/ 404 no-cache + ETag
/%E0%A4%A 400 no-cache"
/%E0%A4%A/ 400 no-cache"
`);
});

Expand Down
75 changes: 43 additions & 32 deletions packages/docusaurus/src/commands/serve/requestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import serveHandler from 'serve-handler';
import {pathExists} from '@docusaurus/fs';
import {applyTrailingSlash} from '@docusaurus/utils-common';
import {parseURLPath, serializeURLPath} from '@docusaurus/utils';
import type {URLPath} from '@docusaurus/utils';
import type http from 'node:http';
import type {DocusaurusConfig} from '@docusaurus/types';

Expand Down Expand Up @@ -101,48 +102,58 @@ async function getRedirectLocation(
trailingSlash,
}: {outDir: string} & Pick<DocusaurusConfig, 'baseUrl' | 'trailingSlash'>,
): Promise<string | undefined> {
// We do the redirect ourselves for a good reason
// server-handler is annoying and won't include /baseUrl/ in redirects
// We do all the redirects ourselves for a good reason
// serve-handler redirects drop the /baseUrl/ and the query string
// See https://github.com/meta/docusaurus/issues/10078#issuecomment-2084932934
if (baseUrl !== '/') {
const {pathname, search} = parseURLPath(url);
// ".." can resolve outside of the baseUrl, even to another host: "//host"
// Like without a baseUrl, serve-handler answers 400 to these paths
if (!pathname.startsWith(baseUrl)) {
return undefined;
}
const urlPath = parseRootURLPath(url);
// ".." can resolve outside of the baseUrl
// Like serve-handler, answer 400 to these paths
if (!urlPath?.pathname.startsWith(baseUrl)) {
return undefined;
}
const {pathname, search} = urlPath;

// Merge repeated slashes, like the serve-handler redirects
const mergedPathname = pathname.replace(/\/{2,}/g, '/');
if (pathname !== mergedPathname) {
return serializeURLPath({pathname: mergedPathname, search});
}
// Merge repeated slashes, like the serve-handler redirects
const mergedPathname = pathname.replace(/\/{2,}/g, '/');
if (pathname !== mergedPathname) {
return serializeURLPath({pathname: mergedPathname, search});
}

const cleanPathname = getCleanPathname(pathname, baseUrl);
if (pathname !== cleanPathname) {
return serializeURLPath({pathname: cleanPathname, search});
}
const cleanPathname = getCleanPathname(pathname, baseUrl);
if (pathname !== cleanPathname) {
return serializeURLPath({pathname: cleanPathname, search});
}

// Not super robust, but should be good enough for our use case
// See https://github.com/meta/docusaurus/pull/10090
const looksLikeAsset = !!pathname.match(/\.[a-z\d]+$/i);
const isAsset =
looksLikeAsset &&
!(await isPage(outDir, pathname.slice(baseUrl.length - 1)));
if (!isAsset) {
const normalizedPathname = applyTrailingSlash(pathname, {
trailingSlash,
baseUrl,
});
if (pathname !== normalizedPathname) {
return serializeURLPath({pathname: normalizedPathname, search});
}
// Not super robust, but should be good enough for our use case
// See https://github.com/meta/docusaurus/pull/10090
const looksLikeAsset = !!pathname.match(/\.[a-z\d]+$/i);
const isAsset =
looksLikeAsset &&
!(await isPage(outDir, pathname.slice(baseUrl.length - 1)));
if (!isAsset) {
const normalizedPathname = applyTrailingSlash(pathname, {
trailingSlash,
baseUrl,
});
if (pathname !== normalizedPathname) {
return serializeURLPath({pathname: normalizedPathname, search});
}
}

return undefined;
}

// Like parseURLPath(), but undefined when ".." segments go above the root
// It also keeps "//host/path" as a path instead of parsing another host
function parseRootURLPath(url: string): URLPath | undefined {
const fakeRoot = '/__docusaurus_root__';
const {pathname, search} = parseURLPath(`.${url}`, `${fakeRoot}/`);
if (!pathname.startsWith(`${fakeRoot}/`)) {
return undefined;
}
return {pathname: pathname.slice(fakeRoot.length), search};
}

// Pages with a dot in their name look like assets, like "docs/2.x"
async function isPage(outDir: string, urlPath: string): Promise<boolean> {
let filePath: string;
Expand Down
Loading