| 60 | } |
| 61 | |
| 62 | func (t *inMemoryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { |
| 63 | // The in-process transport requires the caller to have placed the |
| 64 | // delegated API key ID on the context. Without it, aibridged has no |
| 65 | // identity to act under. Fail fast at the transport boundary so the |
| 66 | // handler can assume the invariant. |
| 67 | if _, ok := aibridge.DelegatedAPIKeyIDFromContext(req.Context()); !ok { |
| 68 | return nil, xerrors.New("aibridged in-memory transport requires WithDelegatedAPIKeyID on the request context") |
| 69 | } |
| 70 | |
| 71 | // Adapt the caller's upstream-shaped URL to the daemon's mount layout: |
| 72 | // "/api/v2/aibridge/<providerName>/<original-path>". Done here so |
| 73 | // callers do not need to encode the mount prefix or the provider |
| 74 | // routing key into the requests they hand to the transport. |
| 75 | newPath, err := url.JoinPath(aibridgeRootPath, t.providerName, req.URL.Path) |
| 76 | if err != nil { |
| 77 | return nil, xerrors.Errorf("rewrite request URL for provider %q: %w", t.providerName, err) |
| 78 | } |
| 79 | req = req.Clone(req.Context()) |
| 80 | req.URL.Path = newPath |
| 81 | |
| 82 | pr, pw := io.Pipe() |
| 83 | rw := &pipeResponseWriter{ |
| 84 | header: http.Header{}, |
| 85 | body: pw, |
| 86 | gotHeaders: make(chan struct{}), |
| 87 | status: http.StatusOK, |
| 88 | } |
| 89 | |
| 90 | // Cloning preserves caller-supplied headers and context but lets the |
| 91 | // handler operate on its own request value without surprising the caller |
| 92 | // if it mutates Headers or stores the request. The Source is attached to |
| 93 | // the served context so downstream handlers can log the call site. |
| 94 | served := req.Clone(aibridge.WithSource(req.Context(), t.source)) |
| 95 | |
| 96 | handlerDone := make(chan struct{}) |
| 97 | go func() { |
| 98 | defer func() { |
| 99 | if r := recover(); r != nil { |
| 100 | // Mirror net/http.Server behavior: a panicking handler |
| 101 | // produces a 500 instead of crashing the process. |
| 102 | rw.WriteHeader(http.StatusInternalServerError) |
| 103 | _ = pw.CloseWithError(xerrors.Errorf("handler panicked: %v", r)) |
| 104 | } |
| 105 | // Make sure we always unblock RoundTrip even if the handler |
| 106 | // returns before writing headers (e.g. handler returns early |
| 107 | // without writing). |
| 108 | rw.ensureHeaders() |
| 109 | // If the request context was canceled, surface that as a |
| 110 | // body-read error so the caller sees a network-style failure |
| 111 | // rather than EOF. Otherwise close cleanly. |
| 112 | if cerr := served.Context().Err(); cerr != nil { |
| 113 | _ = pw.CloseWithError(cerr) |
| 114 | } else { |
| 115 | _ = pw.Close() |
| 116 | } |
| 117 | close(handlerDone) |
| 118 | }() |
| 119 | t.handler.ServeHTTP(rw, served) |