Skip to content

fix(inference-gateway): preserve backend authentication errors#895

Open
anastasia-nesterenko wants to merge 1 commit into
mainfrom
anesterenko/aircore-345-inference-gateway-wraps-backend-401-unauthorized-in-502
Open

fix(inference-gateway): preserve backend authentication errors#895
anastasia-nesterenko wants to merge 1 commit into
mainfrom
anesterenko/aircore-345-inference-gateway-wraps-backend-401-unauthorized-in-502

Conversation

@anastasia-nesterenko

@anastasia-nesterenko anastasia-nesterenko commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Preserve backend 401 Unauthorized and 403 Forbidden responses in the inference gateway instead of wrapping them in 502 Bad Gateway.

This allows downstream consumers to identify authentication failures correctly, avoid retrying them as transient errors, and surface the original backend error to users.

Changes

  • Propagate backend 401 and 403 status codes.
  • Preserve the complete backend response body without JSON rewriting or truncation.
  • Preserve response headers such as Content-Type and WWW-Authenticate.
  • Recalculate body-framing headers after aiohttp buffering/decompression.
  • Preserve authentication status when reading a malformed or truncated response body fails.
  • Bypass model rewriting and response/post-response middleware for authentication failures.
  • Return mock-provider authentication responses directly, including streaming responses.
  • Keep backend 404 responses mapped to 502.
  • Consolidate authentication status handling and error-response helpers.

Test coverage

Added coverage for:

  • 401 and 403 responses
  • Provider and virtual-model proxy paths
  • JSON and non-JSON/HTML bodies
  • Empty and bodies larger than the diagnostic logging limit
  • Failed upstream body reads
  • Content-Type and WWW-Authenticate propagation
  • Mock-provider JSON and streaming responses
  • Response middleware bypass
  • Existing non-authentication error behavior

Verification

  • Full inference-gateway suite: 557 passed, 5 skipped
  • Focused proxy and provider-router suite: 117 passed
  • Ruff lint: passed
  • Ruff formatting: passed
  • git diff --cached --check: passed

Linear

AIRCORE-345 — Inference gateway wraps backend 401 Unauthorized in 502

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of upstream authentication failures, preserving status codes, response bodies, and authentication headers.
    • Standardized backend error responses, including clearer handling of unavailable models and malformed responses.
    • Prevented authentication errors from being altered by model processing or response middleware.
    • Fixed response header and streaming behavior for more reliable proxy responses.
  • Tests

    • Expanded coverage for authentication, error propagation, streaming, headers, and middleware behavior.

Signed-off-by: anastasia-nesterenko <anesterenko@nvidia.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The inference gateway now preserves backend 401/403 responses and bodies, wraps backend 404 responses as 502, filters rewritten response headers, treats typed model results as non-streaming, and bypasses VirtualModel rewriting and middleware for authentication failures. Tests cover these behaviors across proxy and provider routes.

Inference gateway error propagation

Layer / File(s) Summary
Error response contracts and buffering
services/core/inference-gateway/src/nmp/core/inference_gateway/api/proxy.py
Proxy helpers buffer authentication bodies, preserve relevant headers, truncate decoded error details, wrap backend 404 responses, and update return contracts.
VirtualModel response routing
services/core/inference-gateway/src/nmp/core/inference_gateway/api/proxy.py
VirtualModel mock and real-provider paths preserve authentication failures before model rewriting or response middleware; streaming classification and serialized header handling are updated.
Proxy and provider error coverage
services/core/inference-gateway/tests/unit/test_proxy.py, services/core/inference-gateway/tests/unit/test_provider_router.py
Tests verify authentication status, body, and header preservation, failed body reads, middleware bypasses, mock-provider errors, and streaming response types.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant virtual_model_proxy
  participant fetch_proxy_response
  participant Backend
  participant ResponseMiddleware
  Client->>virtual_model_proxy: submit inference request
  virtual_model_proxy->>fetch_proxy_response: fetch provider response
  fetch_proxy_response->>Backend: forward request
  Backend-->>fetch_proxy_response: status, headers, body
  fetch_proxy_response-->>virtual_model_proxy: auth bytes or response result
  alt 401 or 403
    virtual_model_proxy-->>Client: buffered upstream error response
  else successful response
    virtual_model_proxy->>ResponseMiddleware: parse and process response
    ResponseMiddleware-->>Client: rewritten model response
  end
Loading

Suggested reviewers: benmccown, ironcommit, mckornfield

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: preserving backend 401/403 authentication errors in inference-gateway.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch anesterenko/aircore-345-inference-gateway-wraps-backend-401-unauthorized-in-502

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
services/core/inference-gateway/src/nmp/core/inference_gateway/api/proxy.py (1)

369-394: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared backend-error policy.

Lines 369-394 and 516-540 are identical apart from the auth return shape, and the mock branch at 898-911 restates the same 401/403/404 policy a third time. A single helper keeps them from drifting:

async def _handle_backend_error(
    response: aiohttp.ClientResponse, url: str
) -> tuple[bytes, CIMultiDict[str], int]:
    """Return (raw_body, headers, status) for auth failures; raise otherwise."""
    status = response.status
    headers = _filter_response_headers(response.headers) if status in _BACKEND_AUTH_ERROR_STATUSES else CIMultiDict()
    raw_body, error_body = await _read_error_body(response)
    _close_response(response)
    logger.warning("Backend error %d from %s: %s", status, url, error_body)
    if status in _BACKEND_AUTH_ERROR_STATUSES:
        return raw_body, headers, status
    if status == 404:
        detail = f"Backend returned 404: {error_body}" if error_body else "Backend returned 404"
        raise HTTPException(status_code=502, detail=detail)
    raise HTTPException(status_code=status, detail=error_body or str(status))

Callers then become raw, hdrs, st = await _handle_backend_error(response, next_request_info.url) plus the local return shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/core/inference-gateway/src/nmp/core/inference_gateway/api/proxy.py`
around lines 369 - 394, Extract the duplicated backend-error handling from the
proxy request paths into a shared async helper named _handle_backend_error. Have
it read and close the response, log the backend error, preserve filtered headers
and raw-body returns for auth statuses, and raise the existing 404/other-status
HTTPException policies otherwise. Replace the handling in the affected callers,
including the mock branch, while preserving each caller’s local response shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/core/inference-gateway/src/nmp/core/inference_gateway/api/proxy.py`:
- Around line 324-331: Update _raw_response to preserve duplicate upstream
response headers by filtering _BODY_FRAMING_HEADERS_TO_STRIP and appending the
remaining header pairs to the Response raw_headers representation, rather than
converting headers to a dict. Keep the existing body, status code, and
framing-header filtering behavior unchanged.

---

Nitpick comments:
In `@services/core/inference-gateway/src/nmp/core/inference_gateway/api/proxy.py`:
- Around line 369-394: Extract the duplicated backend-error handling from the
proxy request paths into a shared async helper named _handle_backend_error. Have
it read and close the response, log the backend error, preserve filtered headers
and raw-body returns for auth statuses, and raise the existing 404/other-status
HTTPException policies otherwise. Replace the handling in the affected callers,
including the mock branch, while preserving each caller’s local response shape.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 329076ab-09fe-4255-afe9-7cf4c7cf2fb9

📥 Commits

Reviewing files that changed from the base of the PR and between 9fa8f2a and 161ee35.

📒 Files selected for processing (3)
  • services/core/inference-gateway/src/nmp/core/inference_gateway/api/proxy.py
  • services/core/inference-gateway/tests/unit/test_provider_router.py
  • services/core/inference-gateway/tests/unit/test_proxy.py

Comment on lines +324 to +331
def _raw_response(
body: bytes | bytearray | memoryview | str,
status_code: int,
headers: CIMultiDict[str],
) -> Response:
"""Return a buffered body with upstream metadata and corrected framing headers."""
safe_headers = {k: v for k, v in headers.items() if k.lower() not in _BODY_FRAMING_HEADERS_TO_STRIP}
return Response(content=body, status_code=status_code, headers=safe_headers)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -A25 'def _filter_response_headers' services/core/inference-gateway/src

Repository: NVIDIA-NeMo/nemo-platform

Length of output: 3313


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=services/core/inference-gateway/src/nmp/core/inference_gateway/api/proxy.py

sed -n '1,340p' "$FILE" | nl -ba | sed -n '1,180p'

printf '\n--- RESPONSE_HEADERS_TO_DROP occurrences ---\n'
rg -n 'RESPONSE_HEADERS_TO_DROP|BODY_FRAMING_HEADERS_TO_STRIP|www-authenticate|set-cookie' "$FILE" services/core/inference-gateway/src -n

Repository: NVIDIA-NeMo/nemo-platform

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
try:
    from starlette.responses import Response
    from multidict import CIMultiDict
except Exception as e:
    print(f"IMPORT_ERROR: {e}")
    raise SystemExit(0)

headers = CIMultiDict([
    ("WWW-Authenticate", "Bearer realm=one"),
    ("WWW-Authenticate", "Basic realm=two"),
    ("X-Test", "a"),
    ("X-Test", "b"),
])
safe_headers = {k: v for k, v in headers.items() if k.lower() not in {"content-length", "content-encoding", "transfer-encoding"}}
resp = Response(content=b"x", headers=safe_headers)
print("safe_headers:", safe_headers)
print("dict items from CIMultiDict:", list(headers.items()))
print("response.raw_headers:", resp.raw_headers)
PY

Repository: NVIDIA-NeMo/nemo-platform

Length of output: 491


Preserve repeated response headers. dict collapses duplicate names, so backend WWW-Authenticate challenges and Set-Cookie entries are reduced to the last value. Append the filtered headers to raw_headers instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/core/inference-gateway/src/nmp/core/inference_gateway/api/proxy.py`
around lines 324 - 331, Update _raw_response to preserve duplicate upstream
response headers by filtering _BODY_FRAMING_HEADERS_TO_STRIP and appending the
remaining header pairs to the Response raw_headers representation, rather than
converting headers to a dict. Keep the existing body, status code, and
framing-header filtering behavior unchanged.

@github-actions

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 27158/34869 77.9% 62.1%
Integration Tests 15965/33581 47.5% 20.0%

response_headers = (
_filter_response_headers(response.headers)
if response_status in _BACKEND_AUTH_ERROR_STATUSES
else CIMultiDict()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

confused, if we're NOT in error states, don't we still want the headers?

response_headers = (
_filter_response_headers(response.headers)
if response_status in _BACKEND_AUTH_ERROR_STATUSES
else CIMultiDict()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same q as above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants