| 150 | } |
| 151 | |
| 152 | private async handle(req: Request): Promise<Response> { |
| 153 | const url = new URL(req.url); |
| 154 | const method = req.method; |
| 155 | |
| 156 | // Accept both /messages and /v1/messages (depending on how baseURL is configured). |
| 157 | const isMessages = url.pathname === "/messages" || url.pathname === "/v1/messages"; |
| 158 | |
| 159 | if (method === "POST" && isMessages) { |
| 160 | let body: Record<string, unknown> = {}; |
| 161 | try { |
| 162 | body = (await req.json()) as Record<string, unknown>; |
| 163 | } catch { |
| 164 | body = {}; |
| 165 | } |
| 166 | |
| 167 | const headers: Record<string, string> = {}; |
| 168 | req.headers.forEach((value, key) => { |
| 169 | headers[key] = value; |
| 170 | }); |
| 171 | |
| 172 | this.captured.push({ |
| 173 | receivedAt: Date.now(), |
| 174 | method, |
| 175 | path: url.pathname, |
| 176 | headers, |
| 177 | body, |
| 178 | }); |
| 179 | |
| 180 | // Matcher routing: first-match-wins. Matchers can return tailored |
| 181 | // responses based on request body (e.g. slow down historian calls). |
| 182 | let matcherResponse: MockResponse | null = null; |
| 183 | for (const matcher of this.matchers) { |
| 184 | const resp = matcher(body, headers); |
| 185 | if (resp !== null) { |
| 186 | matcherResponse = resp; |
| 187 | break; |
| 188 | } |
| 189 | } |
| 190 | const scripted = matcherResponse ?? this.responses.shift() ?? this.defaultResponse; |
| 191 | if (!scripted) { |
| 192 | return new Response( |
| 193 | JSON.stringify({ |
| 194 | type: "error", |
| 195 | error: { type: "mock_error", message: "No scripted response available" }, |
| 196 | }), |
| 197 | { |
| 198 | status: 500, |
| 199 | headers: { "content-type": "application/json" }, |
| 200 | }, |
| 201 | ); |
| 202 | } |
| 203 | |
| 204 | if (scripted.delayMs && scripted.delayMs > 0) { |
| 205 | await Bun.sleep(scripted.delayMs); |
| 206 | } |
| 207 | |
| 208 | // Error response: emit an Anthropic-shaped error body with the |
| 209 | // requested HTTP status. This bypasses the SSE/streaming path |