-
Notifications
You must be signed in to change notification settings - Fork 0
fix(console): stop blaming the data API for chat-route failures #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,17 +7,56 @@ import type { | |
| ChatThreadRecord, | ||
| } from '@/lib/chat/project-types'; | ||
|
|
||
| async function readJson<T>(response: Response): Promise<T> { | ||
| /** | ||
| * A chat-route failure that keeps its evidence instead of flattening it into a | ||
| * string. | ||
| * | ||
| * These routes are the console's own (`/api/chat/*`). They are NOT the data | ||
| * API. Throwing a bare Error meant the caller had nothing but a message to go | ||
| * on, so ChatPage labelled every one of them "The data API is unreachable." and | ||
| * pointed the reader at a service that was answering fine. | ||
| */ | ||
| export class ChatWireError extends Error { | ||
| /** The console route dialed, e.g. '/api/chat/projects'. */ | ||
| readonly door: string; | ||
| /** HTTP status, or null when the request never landed at all. */ | ||
| readonly status: number | null; | ||
| /** The wire code the route named, when it named one. */ | ||
| readonly wireCode: string | null; | ||
|
|
||
| constructor(options: { | ||
| message: string; | ||
| door: string; | ||
| status: number | null; | ||
| wireCode?: string | null; | ||
| }) { | ||
| super(options.message); | ||
| this.name = 'ChatWireError'; | ||
| this.door = options.door; | ||
| this.status = options.status; | ||
| this.wireCode = options.wireCode ?? null; | ||
| } | ||
| } | ||
|
|
||
| async function readJson<T>(response: Response, door: string): Promise<T> { | ||
| if (!response.ok) { | ||
| const body = await response.json().catch(() => ({})) as { message?: string; error?: string }; | ||
| throw new Error(body.message ?? body.error ?? `chat catalog failed: ${response.status}`); | ||
| const body = (await response.json().catch(() => ({}))) as { | ||
| message?: string; | ||
| error?: string; | ||
| }; | ||
| throw new ChatWireError({ | ||
| message: body.message ?? body.error ?? `chat request failed: ${response.status}`, | ||
| door, | ||
| status: response.status, | ||
| wireCode: body.error ?? null, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the object seam returns a known code without a message, Useful? React with 👍 / 👎. |
||
| }); | ||
| } | ||
| return response.json() as Promise<T>; | ||
| } | ||
|
|
||
| export async function fetchChatCatalog(): Promise<ChatCatalog> { | ||
| const response = await fetch('/api/chat/projects', { cache: 'no-store' }); | ||
| return readJson<ChatCatalog>(response); | ||
| return readJson<ChatCatalog>(response, '/api/chat/projects'); | ||
| } | ||
|
|
||
| export async function saveChatProject( | ||
|
|
@@ -28,7 +67,7 @@ export async function saveChatProject( | |
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(project), | ||
| }); | ||
| return readJson<ChatProject>(response); | ||
| return readJson<ChatProject>(response, '/api/chat/projects'); | ||
| } | ||
|
|
||
| export async function selectChatProject(projectId: string): Promise<ChatCatalog> { | ||
|
|
@@ -37,7 +76,7 @@ export async function selectChatProject(projectId: string): Promise<ChatCatalog> | |
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ activeProjectId: projectId }), | ||
| }); | ||
| return readJson<ChatCatalog>(response); | ||
| return readJson<ChatCatalog>(response, '/api/chat/projects'); | ||
| } | ||
|
|
||
| export async function createChatThread(input: { | ||
|
|
@@ -50,14 +89,14 @@ export async function createChatThread(input: { | |
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(input), | ||
| }); | ||
| return readJson<ChatThreadRecord>(response); | ||
| return readJson<ChatThreadRecord>(response, '/api/chat/threads'); | ||
| } | ||
|
|
||
| export async function fetchChatThread(threadId: string): Promise<ChatThreadRecord> { | ||
| const response = await fetch(`/api/chat/threads/${encodeURIComponent(threadId)}`, { | ||
| cache: 'no-store', | ||
| }); | ||
| return readJson<ChatThreadRecord>(response); | ||
| return readJson<ChatThreadRecord>(response, `/api/chat/threads/${encodeURIComponent(threadId)}`); | ||
| } | ||
|
|
||
| export async function persistChatThread( | ||
|
|
@@ -69,7 +108,7 @@ export async function persistChatThread( | |
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(patch), | ||
| }); | ||
| return readJson<ChatThreadRecord>(response); | ||
| return readJson<ChatThreadRecord>(response, `/api/chat/threads/${encodeURIComponent(threadId)}`); | ||
| } | ||
|
|
||
| export async function persistChatMessages( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fallback handles every non-
ChatWireError, not only a rejectedfetch. If/api/chat/projectsor a thread route answers successfully but its JSON is malformed or truncated,response.json()throws aSyntaxErrorand this branch records no status;degradationForthen tells the user that the route did not answer and attributes it to DNS, the network, or a blocked origin even though the route did answer. Wrap successful-response decoding failures with the response status, or otherwise classify them separately from transport rejection.Useful? React with 👍 / 👎.