fix: allowedUrls should not expect every possible URL. (#1913)

* fix: allowedUrls should not expect every possible URL.

* OpenAPI: Introduce `allowedOrigins` and `filterRequest` options to `createProxy`

---------

Co-authored-by: Fuma Nama <[email protected]>
This commit is contained in:
Aries
2025-05-28 18:30:00 +08:00
committed by GitHub
co-authored by Fuma Nama
parent 9e3223f1e3
commit 9721f6f3d8
3 changed files with 70 additions and 24 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Introduce `allowedOrigins` and `filterRequest` options to `createProxy`. Deprecate `allowedUrls` in favour of new APIs.
@@ -21,7 +21,10 @@ Create a route handler for proxy server.
```ts title="/api/proxy/route.ts"
import { openapi } from '@/lib/source';
export const { GET, HEAD, PUT, POST, PATCH, DELETE } = openapi.createProxy();
export const { GET, HEAD, PUT, POST, PATCH, DELETE } = openapi.createProxy({
// optional, we recommend to set a list of allowed origins for proxied requests
allowedOrigins: ['https://example.com'],
});
```
> Follow the [Getting Started](/docs/ui/openapi) guide if `openapi` server is not yet configured.
+61 -23
View File
@@ -7,10 +7,27 @@ type Proxy = {
};
interface CreateProxyOptions {
/**
* Filter by prefixes of request url
*
* @deprecated Use `allowedOrigins` for filtering origins, or `filterRequest` for more detailed rules.
*/
allowedUrls?: string[];
/**
* Override original request/response with yours
* List of allowed origins to proxy to.
*/
allowedOrigins?: string[];
/**
* Determine if the proxied request is allowed.
*
* @returns true if allowed, otherwise forbidden.
*/
filterRequest?: (request: Request) => boolean;
/**
* Override proxied request/response with yours
*/
overrides?: {
request?: (request: Request) => Request;
@@ -19,7 +36,16 @@ interface CreateProxyOptions {
}
export function createProxy(options: CreateProxyOptions = {}): Proxy {
const { allowedUrls, overrides } = options;
const {
allowedOrigins,
allowedUrls,
filterRequest = (req) => {
return (
!allowedUrls || allowedUrls.some((item) => req.url.startsWith(item))
);
},
overrides,
} = options;
const handlers: Partial<Proxy> = {};
async function handler(req: NextRequest): Promise<Response> {
@@ -27,45 +53,59 @@ export function createProxy(options: CreateProxyOptions = {}): Proxy {
if (!url) {
return Response.json(
'A `url` query parameter is required for proxy url',
'[Proxy] A `url` query parameter is required for proxy url',
{
status: 400,
},
);
}
if (
allowedUrls &&
allowedUrls.every((allowedUrl) => !allowedUrl.startsWith(url))
) {
return Response.json('The given `url` query parameter is not allowed', {
let parsedUrl;
try {
parsedUrl = new URL(url);
} catch {
return Response.json('[Proxy] Invalid `url` parameter value.', {
status: 400,
});
}
let clonedReq = new Request(url, {
if (allowedOrigins && !allowedOrigins.includes(parsedUrl.origin)) {
return Response.json(
`[Proxy] The origin "${parsedUrl.origin}" is not allowed.`,
{
status: 400,
},
);
}
let proxied = new Request(parsedUrl, {
...req,
cache: 'no-cache',
mode: 'cors',
});
if (overrides?.request) {
clonedReq = overrides.request(clonedReq);
proxied = overrides.request(proxied);
}
clonedReq.headers.forEach((_value, originalKey) => {
const key = originalKey.toLowerCase();
const notAllowed = key === 'origin';
if (!filterRequest(proxied)) {
return Response.json('[Proxy] The proxied request is not allowed', {
status: 403,
});
}
if (notAllowed) {
clonedReq.headers.delete(originalKey);
proxied.headers.forEach((_value, originalKey) => {
const key = originalKey.toLowerCase();
if (key === 'origin') {
proxied.headers.delete(originalKey);
}
});
let res = await fetch(clonedReq).catch((e) => new Error(e.toString()));
let res = await fetch(proxied).catch((e) => new Error(e.toString()));
if (res instanceof Error) {
return Response.json(`Failed to proxy request: ${res.message}`, {
status: 400,
return Response.json(`[Proxy] Failed to proxy request: ${res.message}`, {
status: 500,
});
}
@@ -76,17 +116,15 @@ export function createProxy(options: CreateProxyOptions = {}): Proxy {
const headers = new Headers(res.headers);
headers.forEach((_value, originalKey) => {
const key = originalKey.toLowerCase();
const notAllowed =
key.startsWith('access-control-') || key === 'content-encoding';
if (notAllowed) {
if (key.startsWith('access-control-') || key === 'content-encoding') {
headers.delete(originalKey);
}
});
headers.set('X-Forwarded-Host', res.url);
return new Response(res.body, {
...res,
status: res.status,
statusText: res.statusText,
headers,
});
}