From cdb27c1d4fe1d617651004245debf912aa6a2ae1 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Wed, 3 Jun 2026 18:47:04 +0530 Subject: [PATCH] fix: use runtime BACKEND_URL for proxying (#411) * fix: use runtime BACKEND_URL for proxying Fixes #400 * chore: run formatter --- ...84be6596b36_make_email_case_insensitive.py | 26 +++-- .../src/dograh_sdk/_generated_models.py | 4 +- ui/next.config.ts | 5 - ui/src/app/api/v1/[...path]/route.ts | 104 ++++++++++++++++++ ui/src/lib/auth/config.ts | 4 +- ui/src/middleware.ts | 4 +- 6 files changed, 130 insertions(+), 17 deletions(-) create mode 100644 ui/src/app/api/v1/[...path]/route.ts diff --git a/api/alembic/versions/384be6596b36_make_email_case_insensitive.py b/api/alembic/versions/384be6596b36_make_email_case_insensitive.py index a300f47..11357c9 100644 --- a/api/alembic/versions/384be6596b36_make_email_case_insensitive.py +++ b/api/alembic/versions/384be6596b36_make_email_case_insensitive.py @@ -5,28 +5,38 @@ Revises: 6bd9f67ec994 Create Date: 2026-06-02 07:58:00.002359 """ + from typing import Sequence, Union -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. -revision: str = '384be6596b36' -down_revision: Union[str, None] = '6bd9f67ec994' +revision: str = "384be6596b36" +down_revision: Union[str, None] = "6bd9f67ec994" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_users_email'), table_name='users') - op.create_index('ix_users_email_lower', 'users', [sa.literal_column('lower(email)')], unique=True, postgresql_where=sa.text('email IS NOT NULL')) + op.drop_index(op.f("ix_users_email"), table_name="users") + op.create_index( + "ix_users_email_lower", + "users", + [sa.literal_column("lower(email)")], + unique=True, + postgresql_where=sa.text("email IS NOT NULL"), + ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_index('ix_users_email_lower', table_name='users', postgresql_where=sa.text('email IS NOT NULL')) - op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.drop_index( + "ix_users_email_lower", + table_name="users", + postgresql_where=sa.text("email IS NOT NULL"), + ) + op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) # ### end Alembic commands ### diff --git a/sdk/python/src/dograh_sdk/_generated_models.py b/sdk/python/src/dograh_sdk/_generated_models.py index 7f2fa8a..ec27e02 100644 --- a/sdk/python/src/dograh_sdk/_generated_models.py +++ b/sdk/python/src/dograh_sdk/_generated_models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: -# filename: dograh-openapi-rs5H7P.json -# timestamp: 2026-06-02T06:01:29+00:00 +# filename: dograh-openapi-uraOZf.json +# timestamp: 2026-06-03T11:53:30+00:00 from __future__ import annotations diff --git a/ui/next.config.ts b/ui/next.config.ts index 1b8a399..98242c2 100644 --- a/ui/next.config.ts +++ b/ui/next.config.ts @@ -9,11 +9,6 @@ const nextConfig: NextConfig = { }, async rewrites() { return [ - // API proxy for backend calls (excluding Next.js API routes) - { - source: "/api/:path((?!config|auth).*)*", - destination: `${process.env.BACKEND_URL || 'http://localhost:8000'}/api/:path*`, - }, { source: "/ingest/static/:path*", destination: "https://us-assets.i.posthog.com/static/:path*", diff --git a/ui/src/app/api/v1/[...path]/route.ts b/ui/src/app/api/v1/[...path]/route.ts new file mode 100644 index 0000000..7f89b0a --- /dev/null +++ b/ui/src/app/api/v1/[...path]/route.ts @@ -0,0 +1,104 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { getServerBackendUrl } from "@/lib/apiClient"; + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; + +const HOP_BY_HOP_HEADERS = [ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +]; + +function trimTrailingSlash(url: string) { + return url.endsWith("/") ? url.slice(0, -1) : url; +} + +function buildBackendUrl(request: NextRequest) { + const backendUrl = trimTrailingSlash(getServerBackendUrl()); + return `${backendUrl}${request.nextUrl.pathname}${request.nextUrl.search}`; +} + +function createRequestHeaders(request: NextRequest) { + const headers = new Headers(request.headers); + + for (const header of HOP_BY_HOP_HEADERS) { + headers.delete(header); + } + + headers.delete("accept-encoding"); + headers.delete("content-length"); + headers.delete("host"); + + return headers; +} + +function createResponseHeaders(response: Response) { + const headers = new Headers(response.headers); + const setCookies = response.headers.getSetCookie(); + + for (const header of HOP_BY_HOP_HEADERS) { + headers.delete(header); + } + + headers.delete("content-encoding"); + headers.delete("content-length"); + headers.delete("set-cookie"); + + for (const cookie of setCookies) { + headers.append("set-cookie", cookie); + } + + return headers; +} + +async function getRequestBody(request: NextRequest) { + if (request.method === "GET" || request.method === "HEAD") { + return undefined; + } + + return request.arrayBuffer(); +} + +async function proxyRequest(request: NextRequest) { + const backendUrl = buildBackendUrl(request); + + try { + const response = await fetch(backendUrl, { + method: request.method, + headers: createRequestHeaders(request), + body: await getRequestBody(request), + cache: "no-store", + }); + + return new Response(request.method === "HEAD" ? null : response.body, { + status: response.status, + statusText: response.statusText, + headers: createResponseHeaders(response), + }); + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown backend proxy error"; + + return NextResponse.json( + { + detail: `Backend request failed while proxying to ${backendUrl}: ${message}`, + }, + { status: 502 }, + ); + } +} + +export const GET = proxyRequest; +export const POST = proxyRequest; +export const PUT = proxyRequest; +export const PATCH = proxyRequest; +export const DELETE = proxyRequest; +export const OPTIONS = proxyRequest; +export const HEAD = proxyRequest; diff --git a/ui/src/lib/auth/config.ts b/ui/src/lib/auth/config.ts index b58927b..1958297 100644 --- a/ui/src/lib/auth/config.ts +++ b/ui/src/lib/auth/config.ts @@ -1,5 +1,7 @@ import "server-only"; +import { getServerBackendUrl } from "@/lib/apiClient"; + let cachedAuthProvider: string | null = null; /** @@ -12,7 +14,7 @@ export async function getAuthProvider(): Promise { } try { - const backendUrl = process.env.BACKEND_URL || "http://localhost:8000"; + const backendUrl = getServerBackendUrl(); const res = await fetch(`${backendUrl}/api/v1/health`, { next: { revalidate: 300 }, }); diff --git a/ui/src/middleware.ts b/ui/src/middleware.ts index f73231a..12014dc 100644 --- a/ui/src/middleware.ts +++ b/ui/src/middleware.ts @@ -1,6 +1,8 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; +import { getServerBackendUrl } from '@/lib/apiClient'; + const OSS_TOKEN_COOKIE = 'dograh_auth_token'; // Paths that don't require authentication in OSS mode @@ -14,7 +16,7 @@ async function fetchAuthProvider(): Promise { } try { - const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'; + const backendUrl = getServerBackendUrl(); const res = await fetch(`${backendUrl}/api/v1/health`); if (res.ok) { const data = await res.json();