-
-
Notifications
You must be signed in to change notification settings - Fork 0
fix(api): improve error parsing for non-JSON API responses #12
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 |
|---|---|---|
|
|
@@ -377,6 +377,118 @@ describe('fetcher', () => { | |
| ) | ||
| }) | ||
|
|
||
| it('should extract error message from text/plain response', async () => { | ||
| const mockHeaders = new Headers() | ||
| mockHeaders.set('content-type', 'text/plain') | ||
|
|
||
| mockFetch( | ||
| { | ||
| status: 503, | ||
| text: () => Promise.resolve('Service Unavailable'), | ||
| headers: mockHeaders, | ||
| }, | ||
| false | ||
| ) | ||
|
|
||
| await expect(fetcher(DUMMY_URL, DUMMY_TOKEN)).rejects.toThrow( | ||
| 'Service Unavailable' | ||
| ) | ||
| }) | ||
|
|
||
| it('should extract error message from text/html response', async () => { | ||
| const mockHeaders = new Headers() | ||
| mockHeaders.set('content-type', 'text/html') | ||
|
|
||
| mockFetch( | ||
| { | ||
| status: 502, | ||
| text: () => Promise.resolve('<html><body>Bad Gateway</body></html>'), | ||
| headers: mockHeaders, | ||
| }, | ||
| false | ||
| ) | ||
|
|
||
| await expect(fetcher(DUMMY_URL, DUMMY_TOKEN)).rejects.toThrow( | ||
| '<html><body>Bad Gateway</body></html>' | ||
| ) | ||
| }) | ||
|
|
||
| it('should truncate long text error responses', async () => { | ||
| const mockHeaders = new Headers() | ||
| mockHeaders.set('content-type', 'text/html') | ||
|
|
||
| const longHtml = '<html>' + 'x'.repeat(300) + '</html>' | ||
|
|
||
| mockFetch( | ||
| { | ||
| status: 502, | ||
| text: () => Promise.resolve(longHtml), | ||
| headers: mockHeaders, | ||
| }, | ||
| false | ||
| ) | ||
|
|
||
| try { | ||
| await fetcher(DUMMY_URL, DUMMY_TOKEN) | ||
| } catch (err) { | ||
| expect((err as Error).message).toHaveLength(203) // 200 chars + "..." | ||
| expect((err as Error).message.endsWith('...')).toBe(true) | ||
| } | ||
| }) | ||
|
Comment on lines
+431
to
+437
|
||
|
|
||
| it('should use default message when text body is empty', async () => { | ||
| const mockHeaders = new Headers() | ||
| mockHeaders.set('content-type', 'text/plain') | ||
|
|
||
| mockFetch( | ||
| { | ||
| status: 503, | ||
| text: () => Promise.resolve(''), | ||
| headers: mockHeaders, | ||
| }, | ||
| false | ||
| ) | ||
|
|
||
| await expect(fetcher(DUMMY_URL, DUMMY_TOKEN)).rejects.toThrow( | ||
| 'An error occurred while fetching data from the Figma API' | ||
| ) | ||
| }) | ||
|
|
||
| it('should use default message when content-type is null', async () => { | ||
| const mockHeaders = new Headers() | ||
| // No content-type set | ||
|
|
||
| mockFetch( | ||
| { | ||
| status: 500, | ||
| headers: mockHeaders, | ||
| }, | ||
| false | ||
| ) | ||
|
|
||
| await expect(fetcher(DUMMY_URL, DUMMY_TOKEN)).rejects.toThrow( | ||
| 'An error occurred while fetching data from the Figma API' | ||
| ) | ||
| }) | ||
|
|
||
| it('should use default message when text() throws', async () => { | ||
| const mockHeaders = new Headers() | ||
| mockHeaders.set('content-type', 'text/plain') | ||
|
|
||
| mockFetch( | ||
| { | ||
| status: 503, | ||
| text: () => Promise.reject(new Error('Read error')), | ||
| headers: mockHeaders, | ||
| }, | ||
| false | ||
| ) | ||
|
|
||
| await expect(fetcher(DUMMY_URL, DUMMY_TOKEN)).rejects.toThrow( | ||
| 'An error occurred while fetching data from the Figma API' | ||
| ) | ||
| }) | ||
|
|
||
| it('should not parse Retry-After for non-429 errors', async () => { | ||
| const mockHeaders = new Headers() | ||
| mockHeaders.set('content-type', 'application/json') | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -173,6 +173,93 @@ describe('mutator', () => { | |
| ) | ||
| }) | ||
|
|
||
| it('should extract error message from text/plain response', async () => { | ||
| mockFetch.mockResolvedValue({ | ||
| ok: false, | ||
| status: 503, | ||
| headers: { | ||
| get: (name: string) => (name === 'content-type' ? 'text/plain' : null), | ||
| }, | ||
| text: () => Promise.resolve('Service Unavailable'), | ||
| }) | ||
| await expect(mutator(url, token, 'CREATE', body)).rejects.toThrow( | ||
| 'Service Unavailable' | ||
| ) | ||
| }) | ||
|
|
||
| it('should extract error message from text/html response', async () => { | ||
| mockFetch.mockResolvedValue({ | ||
| ok: false, | ||
| status: 502, | ||
| headers: { | ||
| get: (name: string) => (name === 'content-type' ? 'text/html' : null), | ||
| }, | ||
| text: () => Promise.resolve('<html><body>Bad Gateway</body></html>'), | ||
| }) | ||
| await expect(mutator(url, token, 'CREATE', body)).rejects.toThrow( | ||
| '<html><body>Bad Gateway</body></html>' | ||
| ) | ||
| }) | ||
|
|
||
| it('should truncate long text error responses', async () => { | ||
| const longHtml = '<html>' + 'x'.repeat(300) + '</html>' | ||
| mockFetch.mockResolvedValue({ | ||
| ok: false, | ||
| status: 502, | ||
| headers: { | ||
| get: (name: string) => (name === 'content-type' ? 'text/html' : null), | ||
| }, | ||
| text: () => Promise.resolve(longHtml), | ||
| }) | ||
| try { | ||
| await mutator(url, token, 'CREATE', body) | ||
| } catch (err) { | ||
| expect((err as Error).message).toHaveLength(203) // 200 chars + "..." | ||
| expect((err as Error).message.endsWith('...')).toBe(true) | ||
| } | ||
| }) | ||
|
Comment on lines
+214
to
+220
|
||
|
|
||
| it('should use default message when text body is empty', async () => { | ||
| mockFetch.mockResolvedValue({ | ||
| ok: false, | ||
| status: 503, | ||
| headers: { | ||
| get: (name: string) => (name === 'content-type' ? 'text/plain' : null), | ||
| }, | ||
| text: () => Promise.resolve(''), | ||
| }) | ||
| await expect(mutator(url, token, 'CREATE', body)).rejects.toThrow( | ||
| 'An API error occurred' | ||
| ) | ||
| }) | ||
|
|
||
| it('should use default message when content-type is null', async () => { | ||
| mockFetch.mockResolvedValue({ | ||
| ok: false, | ||
| status: 500, | ||
| headers: { | ||
| get: () => null, | ||
| }, | ||
| }) | ||
| await expect(mutator(url, token, 'CREATE', body)).rejects.toThrow( | ||
| 'An API error occurred' | ||
| ) | ||
| }) | ||
|
|
||
| it('should use default message when text() throws', async () => { | ||
| mockFetch.mockResolvedValue({ | ||
| ok: false, | ||
| status: 503, | ||
| headers: { | ||
| get: (name: string) => (name === 'content-type' ? 'text/plain' : null), | ||
| }, | ||
| text: () => Promise.reject(new Error('Read error')), | ||
| }) | ||
| await expect(mutator(url, token, 'CREATE', body)).rejects.toThrow( | ||
| 'An API error occurred' | ||
| ) | ||
| }) | ||
|
|
||
| it('should use default errorMessage when both err and message are falsy', async () => { | ||
| mockFetch.mockResolvedValue({ | ||
| ok: false, | ||
|
|
||
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.
The error message extraction logic for text/plain and text/html responses is duplicated between fetcher.ts and mutator.ts (including the 200-character truncation logic). Consider extracting this into a shared utility function to reduce duplication and improve maintainability. This would make it easier to adjust the truncation length or error handling logic in the future without needing to update both files.