# Client middleware: fail open when qache is unreachable.

These drop-in snippets wrap your HTTP client's transport so every outbound request is routed through qache, with an opt-in fallback to the original upstream when the gateway is unreachable. The wrapper is configured once at client construction, with no per-call-site changes.

qache marks its own failure responses with an `x-qache-error` header. The snippets use that header to tell a qache-internal failure apart from an upstream LLM error, so a real LLM 5xx is never silently retried direct.

Pre-requisite: a qache API key exposed to your process as `QACHE_API_KEY`, either a `qk_` dashboard key from https://qache.cc/dashboard, or an `ak_` agent key from `POST https://operations.qache.cc/api/v1/public/agent-keys` (no sign-in).

## Go (`net/http`)

Drop this file into your project; no third-party dependencies are required.

```go
// qache_transport.go (drop-in, no external dependencies).
package qache

import (
	"bytes"
	"io"
	"net/http"
	"net/url"
)

// Transport is an http.RoundTripper that routes outgoing requests through
// the qache gateway, attaching the X-Qache-API-Key header. Wire it once
// onto each http.Client.Transport; no per-call changes are needed.
//
//	client := &http.Client{Transport: &qache.Transport{
//	    APIKey:      os.Getenv("QACHE_API_KEY"),
//	    GatewayHost: "gateway.qache.cc",
//	    Fallback:    true,
//	}}
//
// With Fallback=true, the Transport retries the request against the original
// upstream URL if the gateway is unreachable (TCP/DNS error) or returns a
// response carrying an X-Qache-Error header (a qache-internal failure).
// Upstream LLM errors pass through unchanged.
type Transport struct {
	Base        http.RoundTripper // optional; defaults to http.DefaultTransport
	APIKey      string            // X-Qache-API-Key value
	GatewayHost string            // e.g. "gateway.qache.cc"
	Fallback    bool              // on qache-internal error, retry direct to upstream
}

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
	base := t.Base
	if base == nil {
		base = http.DefaultTransport
	}
	if t.APIKey == "" || t.GatewayHost == "" || req.URL.Host == t.GatewayHost {
		return base.RoundTrip(req)
	}

	body, err := drainBody(req)
	if err != nil {
		return nil, err
	}

	routed := cloneRequest(req, body)
	routed.URL = &url.URL{
		Scheme:   "https",
		Host:     t.GatewayHost,
		Path:     "/" + req.URL.Host + req.URL.Path,
		RawQuery: req.URL.RawQuery,
	}
	routed.Host = t.GatewayHost
	routed.Header.Set("X-Qache-API-Key", t.APIKey)

	resp, err := base.RoundTrip(routed)
	if !t.Fallback || !qacheInternalError(resp, err) {
		return resp, err
	}
	if resp != nil {
		_ = resp.Body.Close()
	}
	return base.RoundTrip(cloneRequest(req, body))
}

func qacheInternalError(resp *http.Response, err error) bool {
	if err != nil {
		return true
	}
	return resp != nil && resp.Header.Get("X-Qache-Error") != ""
}

func drainBody(req *http.Request) ([]byte, error) {
	if req.Body == nil || req.Body == http.NoBody {
		return nil, nil
	}
	defer req.Body.Close()
	return io.ReadAll(req.Body)
}

func cloneRequest(req *http.Request, body []byte) *http.Request {
	out := req.Clone(req.Context())
	out.Header = req.Header.Clone()
	if body != nil {
		out.Body = io.NopCloser(bytes.NewReader(body))
		out.ContentLength = int64(len(body))
		out.GetBody = func() (io.ReadCloser, error) {
			return io.NopCloser(bytes.NewReader(body)), nil
		}
	}
	return out
}
```

## Python (`httpx`)

Requires `httpx`; no other dependencies. `httpx.Client(transport=...)` is the natural wiring point; `requests` does not have an equivalent that survives URL host changes cleanly, so prefer `httpx` for raw HTTP code that needs the fallback.

```python
# qache_transport.py (drop-in). Requires httpx; no other dependencies.
import httpx


class Transport(httpx.HTTPTransport):
    """HTTP transport that routes outgoing requests through the qache gateway.

    Wire it once onto an httpx.Client:

        client = httpx.Client(transport=qache_transport.Transport(
            api_key=os.environ["QACHE_API_KEY"],
            gateway_host="gateway.qache.cc",
            fallback=True,
        ))

    With fallback=True, the transport retries the request against the original
    upstream URL if the gateway is unreachable (network error) or returns a
    response carrying an X-Qache-Error header (a qache-internal failure).
    Upstream LLM errors pass through unchanged.
    """

    def __init__(
        self,
        api_key: str,
        gateway_host: str = "gateway.qache.cc",
        fallback: bool = False,
        **kwargs,
    ) -> None:
        super().__init__(**kwargs)
        self._api_key = api_key
        self._gateway_host = gateway_host
        self._fallback = fallback

    def handle_request(self, request: httpx.Request) -> httpx.Response:
        if not self._api_key or request.url.host == self._gateway_host:
            return super().handle_request(request)

        original_url = request.url
        original_host_header = request.headers.get("host")
        body = request.read()

        request.url = original_url.copy_with(
            host=self._gateway_host,
            port=None,
            path="/" + original_url.host + (original_url.path or "/"),
        )
        request.headers["X-Qache-API-Key"] = self._api_key
        if "host" in request.headers:
            request.headers["host"] = self._gateway_host
        request.stream = httpx.ByteStream(body)

        try:
            response = super().handle_request(request)
        except httpx.HTTPError:
            if not self._fallback:
                raise
            return self._direct(request, body, original_url, original_host_header)

        if self._fallback and "x-qache-error" in response.headers:
            response.close()
            return self._direct(request, body, original_url, original_host_header)
        return response

    def _direct(self, request, body, original_url, original_host_header):
        request.url = original_url
        request.headers.pop("X-Qache-API-Key", None)
        if original_host_header is not None:
            request.headers["host"] = original_host_header
        else:
            request.headers.pop("host", None)
        request.stream = httpx.ByteStream(body)
        return super().handle_request(request)
```

## JavaScript / TypeScript (`fetch`)

Uses only the global `fetch` available on Node 18+, Bun, Deno, and the browser. Replace your usage of `fetch` with the function returned by `makeQacheFetch`.

```javascript
// qache.js (drop-in). No dependencies; uses the global fetch.
//
// Returns a fetch-compatible function that routes outgoing requests through
// the qache gateway. Use it in place of the global fetch:
//
//   import { makeQacheFetch } from "./qache.js";
//   const fetch = makeQacheFetch({
//     apiKey: process.env.QACHE_API_KEY,
//     gatewayHost: "gateway.qache.cc",
//     fallback: true,
//   });
//
// With fallback=true, the wrapper retries the request against the original
// upstream URL if the gateway is unreachable (network error) or returns a
// response carrying an X-Qache-Error header (a qache-internal failure).
// Upstream LLM errors pass through unchanged.

export function makeQacheFetch({
  apiKey,
  gatewayHost = "gateway.qache.cc",
  fallback = false,
  baseFetch = fetch,
} = {}) {
  return async function qacheFetch(input, init = {}) {
    const upstream = new URL(typeof input === "string" ? input : input.url);
    if (!apiKey || upstream.host === gatewayHost) {
      return baseFetch(input, init);
    }

    const routed = new URL(upstream.toString());
    routed.protocol = "https:";
    routed.host = gatewayHost;
    routed.pathname = "/" + upstream.host + upstream.pathname;

    const headers = new Headers(init.headers);
    headers.set("X-Qache-API-Key", apiKey);

    let body = init.body;
    if (fallback && body instanceof ReadableStream) {
      body = await new Response(body).arrayBuffer();
    }

    try {
      const response = await baseFetch(routed.toString(), { ...init, headers, body });
      if (!fallback || !response.headers.has("x-qache-error")) {
        return response;
      }
      response.body?.cancel();
    } catch (err) {
      if (!fallback) throw err;
    }

    return baseFetch(upstream.toString(), { ...init, body });
  };
}
```

## Streaming caveat

Fallback only fires before the gateway has sent a `2xx` status line. Once the upstream has started streaming (SSE / NDJSON) and qache has begun forwarding bytes, mid-stream failures surface to the caller as truncated streams; they cannot be retried. This is the same constraint as any other retry-on-failure scheme.
