fix: API Endpoint Backend C_Migration#207
Conversation
There was a problem hiding this comment.
Pull request overview
Introduces a private-networking-friendly deployment pattern where the backend API Container App can be internal-only (when enablePrivateNetworking is enabled) and the frontend proxies API calls via same-origin /api, aligning backend exposure with an App Gateway/WAF front-door model.
Changes:
- Add an
/api/{full_path:path}proxy route to the FastAPI frontend server (usingBACKEND_API_URL) and addhttpxdependency. - Update Bicep to (a) make backend ingress internal-only when private networking is enabled and (b) set frontend env vars to use same-origin
/api+ a conditionalBACKEND_API_URL. - Update deployment documentation to clarify public vs internal reachability in the production WAF configuration.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/frontend/requirements.txt | Adds httpx dependency for the new proxy behavior. |
| src/frontend/frontend_server.py | Implements the /api/* proxy endpoint and reads BACKEND_API_URL. |
| infra/main.bicep | Makes backend ingress conditional on private networking; sets frontend API_URL and BACKEND_API_URL. |
| infra/main_custom.bicep | Mirrors the backend ingress + frontend env var changes; updates backend URI output to be conditional. |
| docs/DeploymentGuide.md | Documents that only the frontend remains publicly reachable in WAF/private networking mode. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client: | ||
| proxied = await client.request( | ||
| method=request.method, | ||
| url=target_url, | ||
| headers=headers, | ||
| content=body, | ||
| ) |
There was a problem hiding this comment.
proxy_api doesn't handle httpx transport errors (timeouts, DNS failures, connection errors). Those exceptions will bubble up as 500s from the frontend rather than returning a clear 502/504 to callers; add try/except httpx.RequestError (and potentially httpx.TimeoutException) and map to an appropriate gateway error response.
| async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client: | |
| proxied = await client.request( | |
| method=request.method, | |
| url=target_url, | |
| headers=headers, | |
| content=body, | |
| ) | |
| try: | |
| async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client: | |
| proxied = await client.request( | |
| method=request.method, | |
| url=target_url, | |
| headers=headers, | |
| content=body, | |
| ) | |
| except httpx.TimeoutException: | |
| return JSONResponse( | |
| status_code=504, | |
| content={"detail": "Upstream backend request timed out"}, | |
| ) | |
| except httpx.RequestError: | |
| return JSONResponse( | |
| status_code=502, | |
| content={"detail": "Failed to reach upstream backend"}, | |
| ) |
| passthrough_headers = { | ||
| key: value | ||
| for key, value in proxied.headers.items() | ||
| if key.lower() not in {"content-encoding", "transfer-encoding", "connection"} | ||
| } | ||
|
|
||
| return Response( | ||
| content=proxied.content, | ||
| status_code=proxied.status_code, | ||
| headers=passthrough_headers, | ||
| media_type=proxied.headers.get("content-type"), | ||
| ) | ||
|
|
||
|
|
There was a problem hiding this comment.
The proxy copies upstream response headers almost verbatim. This should drop content-length (it can become incorrect if httpx decompresses the body and you also remove content-encoding) and should preserve multi-value headers like set-cookie (a dict-comprehension over .items() can collapse duplicates), otherwise clients may see truncated responses or broken cookies.
| passthrough_headers = { | |
| key: value | |
| for key, value in proxied.headers.items() | |
| if key.lower() not in {"content-encoding", "transfer-encoding", "connection"} | |
| } | |
| return Response( | |
| content=proxied.content, | |
| status_code=proxied.status_code, | |
| headers=passthrough_headers, | |
| media_type=proxied.headers.get("content-type"), | |
| ) | |
| excluded_headers = { | |
| "content-encoding", | |
| "transfer-encoding", | |
| "connection", | |
| "content-length", | |
| "content-type", | |
| } | |
| response = Response( | |
| content=proxied.content, | |
| status_code=proxied.status_code, | |
| media_type=proxied.headers.get("content-type"), | |
| ) | |
| for key, value in proxied.headers.multi_items(): | |
| if key.lower() not in excluded_headers: | |
| response.raw_headers.append((key.encode("latin-1"), value.encode("latin-1"))) | |
| return response |
| async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client: | ||
| proxied = await client.request( | ||
| method=request.method, | ||
| url=target_url, | ||
| headers=headers, | ||
| content=body, | ||
| ) |
There was a problem hiding this comment.
A new httpx.AsyncClient is created per request and follow_redirects=True changes redirect semantics (clients will never see 3xx responses). For better performance and more predictable proxy behavior, reuse a single AsyncClient (startup/shutdown lifespan) and consider leaving redirects to the caller (or only following redirects within the backend origin).
- Add try/except for httpx.TimeoutException (504) and httpx.RequestError (502) - Drop content-length from passthrough headers to avoid truncated responses - Use multi_items() to preserve multi-value headers like set-cookie Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This pull request introduces a production-ready networking configuration for containerized applications, focusing on improved security by restricting backend API access to internal networks and implementing a frontend proxy pattern. The main changes include updating the infrastructure-as-code to support private networking, adjusting environment variables, and introducing an API proxy in the frontend server to route requests securely.
Infrastructure and Networking Changes:
Frontend Environment and API Proxy:
API_URLis set to/apifor same-origin calls, and a newBACKEND_API_URLvariable is introduced to handle both internal and public backend URIs based on the networking configuration. [1] [2]frontend_server.py) now includes an API proxy route (/api/{full_path:path}), forwarding API requests to the backend API using theBACKEND_API_URLenvironment variable. This ensures all API traffic passes through the frontend, supporting both internal and external networking scenarios. [1] [2]httpxas a dependency to support asynchronous HTTP proxying in the frontend server.## PurposeDoes this introduce a breaking change?
Golden Path Validation
Deployment Validation
What to Check
Verify that the following are valid
Other Information