mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Support forwarding pure playground cookies with proxy
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Support forwarding pure playground cookies with proxy
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useApiContext, useServerContext } from '@/ui/contexts/api';
|
||||
import type { FetchResult } from '@/playground/fetcher';
|
||||
import type { BrowserFetcherOptions, FetchResult } from '@/playground/fetcher';
|
||||
import type { SecurityEntry } from '@/playground/index';
|
||||
import { getStatusInfo } from './status-info';
|
||||
import { joinURL, resolveRequestData, resolveServerUrl, withBase } from '@/utils/url';
|
||||
@@ -94,8 +94,11 @@ export interface PlaygroundClientOptions {
|
||||
*/
|
||||
transformAuthInputs?: (fields: AuthField[]) => AuthField[];
|
||||
|
||||
fetchOptions?: BrowserFetcherOptions;
|
||||
|
||||
/**
|
||||
* Request timeout in seconds (default: 10s)
|
||||
* @deprecated use `fetchOptions.requestTimeout` instead.
|
||||
*/
|
||||
requestTimeout?: number;
|
||||
|
||||
@@ -160,7 +163,8 @@ export default function PlaygroundClient({
|
||||
client: {
|
||||
playground: {
|
||||
components: { ResultDisplay = DefaultResultDisplay } = {},
|
||||
requestTimeout = 10,
|
||||
requestTimeout,
|
||||
fetchOptions = { requestTimeout },
|
||||
transformAuthInputs,
|
||||
} = {},
|
||||
},
|
||||
@@ -194,7 +198,7 @@ export default function PlaygroundClient({
|
||||
|
||||
const testQuery = useQuery(async (input: FormValues) => {
|
||||
const fetcher = await import('./fetcher').then((mod) =>
|
||||
mod.createBrowserFetcher(mediaAdapters, requestTimeout),
|
||||
mod.createBrowserFetcher(mediaAdapters, { proxyUrl, ...fetchOptions }),
|
||||
);
|
||||
const encoded = encodeRequestData(
|
||||
{ ...mapInputs(input), method, bodyMediaType: body?.mediaType },
|
||||
@@ -209,10 +213,7 @@ export default function PlaygroundClient({
|
||||
),
|
||||
resolveRequestData(route, encoded),
|
||||
),
|
||||
{
|
||||
proxyUrl,
|
||||
...encoded,
|
||||
},
|
||||
encoded,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { RequestData } from '@/requests/types';
|
||||
import type { MediaAdapter } from '@/requests/media/adapter';
|
||||
import { resolveMediaAdapter } from '@/requests/media/adapter';
|
||||
|
||||
export interface FetchOptions extends RequestData {
|
||||
proxyUrl?: string;
|
||||
}
|
||||
import type { Awaitable } from '@/types';
|
||||
|
||||
export interface FetchResult {
|
||||
status: number;
|
||||
@@ -18,63 +15,101 @@ export interface Fetcher {
|
||||
*
|
||||
* @param url - The full URL of request.
|
||||
*/
|
||||
fetch: (url: string, options: FetchOptions) => Promise<FetchResult>;
|
||||
fetch: (url: string, data: RequestData) => Promise<FetchResult>;
|
||||
}
|
||||
|
||||
export interface BrowserFetcherOptions {
|
||||
/**
|
||||
* Request timeout in seconds (default: 10s)
|
||||
*/
|
||||
requestTimeout?: number | false;
|
||||
|
||||
proxyUrl?: string;
|
||||
/**
|
||||
* Forward cookies via search parameters when API proxy is configured.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
proxyForwardCookie?: boolean;
|
||||
|
||||
/**
|
||||
* transform the request options before sending.
|
||||
*/
|
||||
onRequestInit?: (requestInit: RequestInit) => Awaitable<RequestInit>;
|
||||
}
|
||||
|
||||
export function createBrowserFetcher(
|
||||
adapters: Record<string, MediaAdapter>,
|
||||
requestTimeout: number,
|
||||
{
|
||||
proxyUrl,
|
||||
proxyForwardCookie = true,
|
||||
requestTimeout = 10,
|
||||
onRequestInit,
|
||||
}: BrowserFetcherOptions = {},
|
||||
): Fetcher {
|
||||
return {
|
||||
async fetch(url, options) {
|
||||
const headers = new Headers();
|
||||
if (options.bodyMediaType && options.bodyMediaType !== 'multipart/form-data')
|
||||
headers.append('Content-Type', options.bodyMediaType);
|
||||
async fetch(url, data) {
|
||||
let requestUrl = new URL(url, document.baseURI);
|
||||
let requestInit: RequestInit = {
|
||||
method: data.method,
|
||||
cache: 'no-cache',
|
||||
signal:
|
||||
typeof requestTimeout === 'number'
|
||||
? AbortSignal.timeout(requestTimeout * 1000)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
for (const key in options.header) {
|
||||
const param = options.header[key];
|
||||
const headers = (requestInit.headers = new Headers());
|
||||
|
||||
for (const key in data.header) {
|
||||
const param = data.header[key];
|
||||
headers.append(key, param.value);
|
||||
}
|
||||
|
||||
const proxyUrl = options.proxyUrl ? new URL(options.proxyUrl, document.baseURI) : null;
|
||||
|
||||
if (proxyUrl) {
|
||||
proxyUrl.searchParams.append('url', url);
|
||||
url = proxyUrl.toString();
|
||||
requestUrl = new URL(proxyUrl, document.baseURI);
|
||||
requestUrl.searchParams.append('url', url);
|
||||
}
|
||||
|
||||
let body: BodyInit | undefined = undefined;
|
||||
if (options.bodyMediaType && options.body) {
|
||||
const adapter = resolveMediaAdapter(options.bodyMediaType, adapters);
|
||||
if (data.bodyMediaType && data.body) {
|
||||
const adapter = resolveMediaAdapter(data.bodyMediaType, adapters);
|
||||
if (!adapter)
|
||||
return {
|
||||
status: 400,
|
||||
type: 'text',
|
||||
data: `[Fumadocs] No adapter for ${options.bodyMediaType}, you need to specify one from 'createOpenAPI()'.`,
|
||||
data: `[Fumadocs] No adapter for ${data.bodyMediaType}, you need to specify one from 'createOpenAPI()'.`,
|
||||
};
|
||||
|
||||
body = adapter.encode(options as { body: unknown });
|
||||
if (data.bodyMediaType !== 'multipart/form-data') {
|
||||
headers.append('Content-Type', data.bodyMediaType);
|
||||
}
|
||||
|
||||
requestInit.body = adapter.encode(data as { body: unknown });
|
||||
}
|
||||
|
||||
// cookies
|
||||
for (const key in options.cookie) {
|
||||
const param = options.cookie[key];
|
||||
const segs: string[] = [`${key}=${param.value}`];
|
||||
if (proxyUrl && proxyForwardCookie) {
|
||||
const encoded = Object.entries(data.cookie)
|
||||
.map(([k, v]) => `${k}=${encodeURIComponent(v.value)}`)
|
||||
.join('; ');
|
||||
requestUrl.searchParams.set('cookie', encoded);
|
||||
requestInit.credentials = 'omit';
|
||||
} else {
|
||||
for (const key in data.cookie) {
|
||||
const param = data.cookie[key];
|
||||
const segs: string[] = [`${key}=${encodeURIComponent(param.value)}`];
|
||||
|
||||
if (proxyUrl && proxyUrl.origin !== window.location.origin)
|
||||
segs.push(`domain=${proxyUrl.host}`);
|
||||
segs.push('path=/', 'max-age=30');
|
||||
if (proxyUrl && requestUrl.origin !== window.location.origin)
|
||||
segs.push(`domain=${requestUrl.host}`);
|
||||
segs.push('path=/', 'max-age=30');
|
||||
|
||||
document.cookie = segs.join('; ');
|
||||
document.cookie = segs.join('; ');
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
method: options.method,
|
||||
cache: 'no-cache',
|
||||
headers,
|
||||
body,
|
||||
signal: AbortSignal.timeout(requestTimeout * 1000),
|
||||
})
|
||||
if (onRequestInit) requestInit = await onRequestInit(requestInit);
|
||||
|
||||
return fetch(requestUrl, requestInit)
|
||||
.then(async (res) => {
|
||||
const contentType = res.headers.get('Content-Type') ?? '';
|
||||
let type: FetchResult['type'];
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
const keys = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'] as const;
|
||||
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'] as const;
|
||||
const methodsWithBody = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
||||
|
||||
type Proxy = {
|
||||
[K in (typeof keys)[number]]: (req: Request) => Promise<Response>;
|
||||
};
|
||||
type Handler = (req: Request) => Promise<Response>;
|
||||
|
||||
interface CreateProxyOptions {
|
||||
export interface Proxy extends Record<(typeof methods)[number], Handler> {
|
||||
handle: Handler;
|
||||
}
|
||||
|
||||
export interface CreateProxyOptions {
|
||||
/**
|
||||
* Filter by prefixes of request url
|
||||
*
|
||||
@@ -42,45 +45,32 @@ export function createProxy(options: CreateProxyOptions = {}): Proxy {
|
||||
},
|
||||
overrides,
|
||||
} = options;
|
||||
const handlers: Partial<Proxy> = {};
|
||||
const handlers: Partial<Proxy> = {
|
||||
handle: handler,
|
||||
};
|
||||
|
||||
async function handler(req: Request): Promise<Response> {
|
||||
const searchParams = new URL(req.url).searchParams;
|
||||
const url = searchParams.get('url');
|
||||
const rawUrl = searchParams.get('url');
|
||||
|
||||
if (!url)
|
||||
if (!rawUrl)
|
||||
return Response.json('[Proxy] A `url` query parameter is required for proxy url', {
|
||||
status: 400,
|
||||
});
|
||||
|
||||
const parsedUrl = URL.parse(url);
|
||||
if (!parsedUrl)
|
||||
const targetUrl = URL.parse(rawUrl);
|
||||
if (!targetUrl)
|
||||
return Response.json('[Proxy] Invalid `url` parameter value.', {
|
||||
status: 400,
|
||||
});
|
||||
|
||||
if (allowedOrigins && !allowedOrigins.includes(parsedUrl.origin)) {
|
||||
return Response.json(`[Proxy] The origin "${parsedUrl.origin}" is not allowed.`, {
|
||||
if (allowedOrigins && !allowedOrigins.includes(targetUrl.origin)) {
|
||||
return Response.json(`[Proxy] The origin "${targetUrl.origin}" is not allowed.`, {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const contentLength = req.headers.get('content-length');
|
||||
const hasBody = contentLength && parseInt(contentLength) > 0;
|
||||
|
||||
let proxied = new Request(parsedUrl, {
|
||||
method: req.method,
|
||||
cache: 'no-cache',
|
||||
headers: req.headers,
|
||||
body:
|
||||
hasBody && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method.toUpperCase())
|
||||
? await req.arrayBuffer()
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (overrides?.request) {
|
||||
proxied = overrides.request(proxied);
|
||||
}
|
||||
const proxied = await rewriteRequest(req, targetUrl, searchParams.get('cookie'));
|
||||
|
||||
if (!filterRequest(proxied)) {
|
||||
return Response.json('[Proxy] The proxied request is not allowed', {
|
||||
@@ -88,26 +78,51 @@ export function createProxy(options: CreateProxyOptions = {}): Proxy {
|
||||
});
|
||||
}
|
||||
|
||||
proxied.headers.forEach((_value, originalKey) => {
|
||||
const key = originalKey.toLowerCase();
|
||||
try {
|
||||
return rewriteResponse(await fetch(proxied));
|
||||
} catch (err) {
|
||||
return Response.json(
|
||||
`[Proxy] Failed to proxy request: ${err instanceof Error ? err.message : 'unknown reason'}`,
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'origin') {
|
||||
proxied.headers.delete(originalKey);
|
||||
}
|
||||
async function rewriteRequest(
|
||||
request: Request,
|
||||
url: URL,
|
||||
cookie: string | null,
|
||||
): Promise<Request> {
|
||||
const headers = new Headers(request.headers);
|
||||
headers.delete('origin');
|
||||
if (cookie) {
|
||||
headers.set('Cookie', cookie);
|
||||
}
|
||||
|
||||
const contentLength = headers.get('content-length');
|
||||
const hasBody = contentLength && parseInt(contentLength) > 0;
|
||||
|
||||
const proxied = new Request(url, {
|
||||
method: request.method,
|
||||
cache: 'no-cache',
|
||||
headers,
|
||||
body:
|
||||
hasBody && methodsWithBody.has(request.method.toUpperCase())
|
||||
? await request.arrayBuffer()
|
||||
: undefined,
|
||||
});
|
||||
|
||||
let res = await fetch(proxied).catch((e) => new Error(e.toString()));
|
||||
if (res instanceof Error) {
|
||||
return Response.json(`[Proxy] Failed to proxy request: ${res.message}`, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
return overrides?.request ? overrides.request(proxied) : proxied;
|
||||
}
|
||||
|
||||
async function rewriteResponse(response: Response): Promise<Response> {
|
||||
if (overrides?.response) {
|
||||
res = overrides.response(res);
|
||||
response = overrides.response(response);
|
||||
}
|
||||
|
||||
const headers = new Headers(res.headers);
|
||||
const headers = new Headers(response.headers);
|
||||
headers.forEach((_value, originalKey) => {
|
||||
const key = originalKey.toLowerCase();
|
||||
|
||||
@@ -115,16 +130,16 @@ export function createProxy(options: CreateProxyOptions = {}): Proxy {
|
||||
headers.delete(originalKey);
|
||||
}
|
||||
});
|
||||
headers.set('X-Forwarded-Host', res.url);
|
||||
headers.set('X-Forwarded-Host', response.url);
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
for (const key of methods) {
|
||||
handlers[key] = handler;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user