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
40 changes: 40 additions & 0 deletions web/src/app/api/research/_base.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from "vitest"

const { sharedApiBaseUrlMock, sharedProxyJsonMock } = vi.hoisted(() => ({
sharedApiBaseUrlMock: vi.fn(() => "https://backend.test"),
sharedProxyJsonMock: vi.fn(),
}))

vi.mock("../_utils/backend-proxy", () => ({
apiBaseUrl: sharedApiBaseUrlMock,
proxyJson: sharedProxyJsonMock,
}))

import { apiBaseUrl, proxyJson } from "./_base"

describe("research base proxy", () => {
it("delegates backend base URL resolution to the shared helper", () => {
expect(apiBaseUrl()).toBe("https://backend.test")
expect(sharedApiBaseUrlMock).toHaveBeenCalledTimes(1)
})

it("forces backend auth when delegating JSON proxies", async () => {
const req = new Request("https://localhost/api/research/tracks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "demo" }),
})
const response = Response.json({ ok: true }, { status: 201 })
sharedProxyJsonMock.mockResolvedValueOnce(response)

const res = await proxyJson(req, "https://backend/api/research/tracks", "POST")

expect(sharedProxyJsonMock).toHaveBeenCalledWith(
req,
"https://backend/api/research/tracks",
"POST",
{ auth: true },
)
expect(res).toBe(response)
})
})
58 changes: 14 additions & 44 deletions web/src/app/api/research/_base.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,17 @@
export function apiBaseUrl() {
return process.env.PAPERBOT_API_BASE_URL || "http://127.0.0.1:8000"
}
import {
apiBaseUrl as sharedApiBaseUrl,
proxyJson as sharedProxyJson,
type ProxyMethod,
} from "../_utils/backend-proxy"

export async function proxyJson(req: Request, upstreamUrl: string, method: string) {
const body = method === "GET" ? undefined : await req.text()
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 120_000) // 2 min timeout
export function apiBaseUrl(): string {
return sharedApiBaseUrl()
}
Comment on lines +1 to +9

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better code clarity and conciseness, you can directly re-export apiBaseUrl since it's a simple pass-through. This avoids the need for a wrapper function and an import alias.

Suggested change
import {
apiBaseUrl as sharedApiBaseUrl,
proxyJson as sharedProxyJson,
type ProxyMethod,
} from "../_utils/backend-proxy"
export async function proxyJson(req: Request, upstreamUrl: string, method: string) {
const body = method === "GET" ? undefined : await req.text()
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 120_000) // 2 min timeout
export function apiBaseUrl(): string {
return sharedApiBaseUrl()
}
import {
proxyJson as sharedProxyJson,
type ProxyMethod,
} from "../_utils/backend-proxy"
export { apiBaseUrl } from "../_utils/backend-proxy"


try {
const baseHeaders = {
method,
Accept: "application/json",
"Content-Type": req.headers.get("content-type") || "application/json",
} as Record<string, string>
const { withBackendAuth } = await import("../_utils/auth-headers")
const headers = await withBackendAuth(req, baseHeaders)
const upstream = await fetch(upstreamUrl, {
method,
headers,
body,
signal: controller.signal,
})
const text = await upstream.text()
return new Response(text, {
status: upstream.status,
headers: {
"Content-Type": upstream.headers.get("content-type") || "application/json",
"Cache-Control": "no-cache",
},
})
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
const isTimeout = error instanceof Error && error.name === "AbortError"
return Response.json(
{
detail: isTimeout
? `Upstream API timed out: ${upstreamUrl}`
: `Upstream API unreachable: ${upstreamUrl}`,
error: detail,
},
{ status: 502 },
)
} finally {
clearTimeout(timeout)
}
export function proxyJson(
req: Request,
upstreamUrl: string,
method: ProxyMethod,
): Promise<Response> {
return sharedProxyJson(req, upstreamUrl, method, { auth: true })
}
Loading