-
Notifications
You must be signed in to change notification settings - Fork 9
refactor: unify auth and sandbox next proxies #407
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,45 +1,181 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import { withBackendAuth } from "./auth-headers" | ||||||||||||||||||||||||||||||||||||||||||||||
| import { backendBaseUrl, withBackendAuth } from "./auth-headers" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| type ProxyTextOptions = { | ||||||||||||||||||||||||||||||||||||||||||||||
| export type ProxyMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| type ProxyErrorContext = { | ||||||||||||||||||||||||||||||||||||||||||||||
| error: unknown | ||||||||||||||||||||||||||||||||||||||||||||||
| isTimeout: boolean | ||||||||||||||||||||||||||||||||||||||||||||||
| upstreamUrl: string | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| type ProxyOptions = { | ||||||||||||||||||||||||||||||||||||||||||||||
| accept?: string | ||||||||||||||||||||||||||||||||||||||||||||||
| auth?: boolean | ||||||||||||||||||||||||||||||||||||||||||||||
| contentType?: string | ||||||||||||||||||||||||||||||||||||||||||||||
| onError?: (context: ProxyErrorContext) => Response | ||||||||||||||||||||||||||||||||||||||||||||||
| timeoutMs?: number | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| type TextProxyOptions = ProxyOptions & { | ||||||||||||||||||||||||||||||||||||||||||||||
| responseContentType?: string | ||||||||||||||||||||||||||||||||||||||||||||||
| responseHeaders?: HeadersInit | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const DEFAULT_TIMEOUT_MS = 120_000 | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export function apiBaseUrl(): string { | ||||||||||||||||||||||||||||||||||||||||||||||
| return process.env.PAPERBOT_API_BASE_URL || "http://127.0.0.1:8000" | ||||||||||||||||||||||||||||||||||||||||||||||
| return backendBaseUrl() | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export async function proxyJson( | ||||||||||||||||||||||||||||||||||||||||||||||
| req: Request, | ||||||||||||||||||||||||||||||||||||||||||||||
| upstreamUrl: string, | ||||||||||||||||||||||||||||||||||||||||||||||
| method: ProxyMethod, | ||||||||||||||||||||||||||||||||||||||||||||||
| options: TextProxyOptions = {}, | ||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<Response> { | ||||||||||||||||||||||||||||||||||||||||||||||
| return proxyText(req, upstreamUrl, method, { | ||||||||||||||||||||||||||||||||||||||||||||||
| accept: options.accept ?? "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||
| responseContentType: options.responseContentType ?? "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||
| ...options, | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export async function proxyText( | ||||||||||||||||||||||||||||||||||||||||||||||
| req: Request, | ||||||||||||||||||||||||||||||||||||||||||||||
| upstreamUrl: string, | ||||||||||||||||||||||||||||||||||||||||||||||
| method: string, | ||||||||||||||||||||||||||||||||||||||||||||||
| options: ProxyTextOptions = {}, | ||||||||||||||||||||||||||||||||||||||||||||||
| method: ProxyMethod, | ||||||||||||||||||||||||||||||||||||||||||||||
| options: TextProxyOptions = {}, | ||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<Response> { | ||||||||||||||||||||||||||||||||||||||||||||||
| const requestOptions = { | ||||||||||||||||||||||||||||||||||||||||||||||
| accept: "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||
| ...options, | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const upstream = await fetchUpstream(req, upstreamUrl, method, requestOptions) | ||||||||||||||||||||||||||||||||||||||||||||||
| const text = await upstream.text() | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return buildTextResponse(text, upstream, { | ||||||||||||||||||||||||||||||||||||||||||||||
| responseContentType: requestOptions.responseContentType, | ||||||||||||||||||||||||||||||||||||||||||||||
| responseHeaders: requestOptions.responseHeaders, | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return handleProxyError(error, upstreamUrl, requestOptions.onError) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| async function fetchUpstream( | ||||||||||||||||||||||||||||||||||||||||||||||
| req: Request, | ||||||||||||||||||||||||||||||||||||||||||||||
| upstreamUrl: string, | ||||||||||||||||||||||||||||||||||||||||||||||
| method: ProxyMethod, | ||||||||||||||||||||||||||||||||||||||||||||||
| options: ProxyOptions, | ||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<Response> { | ||||||||||||||||||||||||||||||||||||||||||||||
| const normalizedMethod = method.toUpperCase() | ||||||||||||||||||||||||||||||||||||||||||||||
| const headers: Record<string, string> = { | ||||||||||||||||||||||||||||||||||||||||||||||
| Accept: options.accept || "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||
| const controller = options.timeoutMs === 0 ? null : new AbortController() | ||||||||||||||||||||||||||||||||||||||||||||||
| const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS | ||||||||||||||||||||||||||||||||||||||||||||||
| const timeout = controller ? setTimeout(() => controller.abort(), timeoutMs) : null | ||||||||||||||||||||||||||||||||||||||||||||||
| const body = await resolveBody(req, method) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| return await fetch(upstreamUrl, { | ||||||||||||||||||||||||||||||||||||||||||||||
| method, | ||||||||||||||||||||||||||||||||||||||||||||||
| headers: await resolveHeaders(req, body, options), | ||||||||||||||||||||||||||||||||||||||||||||||
| body, | ||||||||||||||||||||||||||||||||||||||||||||||
| signal: controller?.signal, | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (timeout) { | ||||||||||||||||||||||||||||||||||||||||||||||
| clearTimeout(timeout) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| let body: string | undefined | ||||||||||||||||||||||||||||||||||||||||||||||
| if (normalizedMethod !== "GET" && normalizedMethod !== "HEAD") { | ||||||||||||||||||||||||||||||||||||||||||||||
| body = await req.text() | ||||||||||||||||||||||||||||||||||||||||||||||
| headers["Content-Type"] = req.headers.get("content-type") || "application/json" | ||||||||||||||||||||||||||||||||||||||||||||||
| async function resolveHeaders( | ||||||||||||||||||||||||||||||||||||||||||||||
| req: Request, | ||||||||||||||||||||||||||||||||||||||||||||||
| body: string | undefined, | ||||||||||||||||||||||||||||||||||||||||||||||
| options: ProxyOptions, | ||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<HeadersInit> { | ||||||||||||||||||||||||||||||||||||||||||||||
| const headers: Record<string, string> = {} | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (options.accept) { | ||||||||||||||||||||||||||||||||||||||||||||||
| headers.Accept = options.accept | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const upstreamHeaders = options.auth ? await withBackendAuth(req, headers) : headers | ||||||||||||||||||||||||||||||||||||||||||||||
| const upstream = await fetch(upstreamUrl, { | ||||||||||||||||||||||||||||||||||||||||||||||
| method: normalizedMethod, | ||||||||||||||||||||||||||||||||||||||||||||||
| headers: upstreamHeaders, | ||||||||||||||||||||||||||||||||||||||||||||||
| body, | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| const text = await upstream.text() | ||||||||||||||||||||||||||||||||||||||||||||||
| if (body !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||
| headers["Content-Type"] = | ||||||||||||||||||||||||||||||||||||||||||||||
| options.contentType || req.headers.get("content-type") || "application/json" | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (options.auth) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return withBackendAuth(req, headers) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return headers | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| async function resolveBody( | ||||||||||||||||||||||||||||||||||||||||||||||
| req: Request, | ||||||||||||||||||||||||||||||||||||||||||||||
| method: ProxyMethod, | ||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<string | undefined> { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (method === "GET" || method === "HEAD" || req.body === null) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return undefined | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return req.text() | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| function buildTextResponse( | ||||||||||||||||||||||||||||||||||||||||||||||
| text: string, | ||||||||||||||||||||||||||||||||||||||||||||||
| upstream: Response, | ||||||||||||||||||||||||||||||||||||||||||||||
| options: Pick<TextProxyOptions, "responseContentType" | "responseHeaders">, | ||||||||||||||||||||||||||||||||||||||||||||||
| ): Response { | ||||||||||||||||||||||||||||||||||||||||||||||
| const headers = new Headers(options.responseHeaders) | ||||||||||||||||||||||||||||||||||||||||||||||
| headers.set("Cache-Control", "no-cache") | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const contentType = | ||||||||||||||||||||||||||||||||||||||||||||||
| upstream.headers.get("content-type") || options.responseContentType | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (contentType && upstream.status !== 204 && text.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||
| headers.set("Content-Type", contentType) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (upstream.status === 204 || text.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return new Response(null, { | ||||||||||||||||||||||||||||||||||||||||||||||
| status: upstream.status, | ||||||||||||||||||||||||||||||||||||||||||||||
| headers, | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+134
to
+146
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return new Response(text, { | ||||||||||||||||||||||||||||||||||||||||||||||
| status: upstream.status, | ||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||
| "Content-Type": upstream.headers.get("content-type") || options.responseContentType || "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||
| "Cache-Control": "no-cache", | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| headers, | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| function handleProxyError( | ||||||||||||||||||||||||||||||||||||||||||||||
| error: unknown, | ||||||||||||||||||||||||||||||||||||||||||||||
| upstreamUrl: string, | ||||||||||||||||||||||||||||||||||||||||||||||
| onError?: (context: ProxyErrorContext) => Response, | ||||||||||||||||||||||||||||||||||||||||||||||
| ): Response { | ||||||||||||||||||||||||||||||||||||||||||||||
| const isTimeout = error instanceof Error && error.name === "AbortError" | ||||||||||||||||||||||||||||||||||||||||||||||
| const context = { | ||||||||||||||||||||||||||||||||||||||||||||||
| error, | ||||||||||||||||||||||||||||||||||||||||||||||
| isTimeout, | ||||||||||||||||||||||||||||||||||||||||||||||
| upstreamUrl, | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (onError) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return onError(context) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const detail = error instanceof Error ? error.message : String(error) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return Response.json( | ||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||
| detail: isTimeout | ||||||||||||||||||||||||||||||||||||||||||||||
| ? `Upstream API timed out: ${upstreamUrl}` | ||||||||||||||||||||||||||||||||||||||||||||||
| : `Upstream API unreachable: ${upstreamUrl}`, | ||||||||||||||||||||||||||||||||||||||||||||||
| error: detail, | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+170
to
+177
|
||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| { status: 502 }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+170
to
+180
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The default error handler exposes internal error details to the client via the
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,5 @@ | ||
| import { NextRequest, NextResponse } from "next/server" | ||
| import { backendBaseUrl } from "@/app/api/_utils/auth-headers" | ||
| import { apiBaseUrl, proxyJson } from "@/app/api/_utils/backend-proxy" | ||
|
|
||
| export async function POST(req: NextRequest) { | ||
| const body = await req.json() | ||
| const res = await fetch(`${backendBaseUrl()}/api/auth/forgot-password`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify(body), | ||
| }) | ||
| const data = await res.json().catch(() => null) | ||
| return NextResponse.json(data, { status: res.status }) | ||
| export async function POST(req: Request) { | ||
| return proxyJson(req, `${apiBaseUrl()}/api/auth/forgot-password`, "POST") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,9 @@ | ||
| export const runtime = "nodejs" | ||
|
|
||
| import { backendBaseUrl } from "../../_utils/auth-headers" | ||
| import { apiBaseUrl, proxyJson } from "@/app/api/_utils/backend-proxy" | ||
|
|
||
| export async function POST(req: Request) { | ||
| const body = await req.text() | ||
| const res = await fetch(`${backendBaseUrl()}/api/auth/login`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body, | ||
| }).catch(() => null) | ||
|
|
||
| if (!res) return Response.json({ detail: "Service unavailable" }, { status: 502 }) | ||
| const text = await res.text() | ||
| return new Response(text, { | ||
| status: res.status, | ||
| headers: { "Content-Type": "application/json" }, | ||
| return proxyJson(req, `${apiBaseUrl()}/api/auth/login`, "POST", { | ||
| onError: () => Response.json({ detail: "Service unavailable" }, { status: 502 }), | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoding
Cache-Control: no-cachemight be too restrictive. It prevents caching of proxied responses, even if the upstream API sends its own caching headers. Consider respecting the upstream'sCache-Controlheader if it's present, and only falling back tono-cacheas a default. This would make the proxy helper more flexible and allow leveraging backend caching strategies.