Skip to content

Commit be21e84

Browse files
committed
fix(postgrest): expose fetch error causes in details field
1 parent c1bb5f0 commit be21e84

File tree

2 files changed

+43
-34
lines changed

2 files changed

+43
-34
lines changed

packages/core/postgrest-js/src/PostgrestBuilder.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -210,32 +210,36 @@ export default abstract class PostgrestBuilder<
210210
})
211211
if (!this.shouldThrowOnError) {
212212
res = res.catch((fetchError) => {
213-
// Extract cause information if available (e.g., DNS errors, network failures)
214-
const cause = fetchError?.cause
215-
const causeCode = cause?.code ?? ''
216-
const causeMessage = cause?.message ?? ''
217-
218-
// Prefer the underlying cause code (e.g., ENOTFOUND) over the wrapper error code
219-
const errorCode = causeCode || fetchError?.code || ''
213+
// Build detailed error information including cause if available
214+
// Note: We don't populate code/hint for client-side network errors since those
215+
// fields are meant for upstream service errors (PostgREST/PostgreSQL)
216+
let errorDetails = ''
220217

221-
// Build a detailed error message that includes cause information
222-
let errorDetails = fetchError?.stack ?? ''
218+
// Add cause information if available (e.g., DNS errors, network failures)
219+
const cause = fetchError?.cause
223220
if (cause) {
221+
const causeMessage = cause?.message ?? ''
222+
const causeCode = cause?.code ?? ''
223+
224+
errorDetails = `${fetchError?.name ?? 'FetchError'}: ${fetchError?.message}`
224225
errorDetails += `\n\nCaused by: ${cause?.name ?? 'Error'}: ${causeMessage}`
226+
if (causeCode) {
227+
errorDetails += ` (${causeCode})`
228+
}
225229
if (cause?.stack) {
226230
errorDetails += `\n${cause.stack}`
227231
}
228-
if (causeCode) {
229-
errorDetails += `\nError code: ${causeCode}`
230-
}
232+
} else {
233+
// No cause available, just include the error stack
234+
errorDetails = fetchError?.stack ?? ''
231235
}
232236

233237
return {
234238
error: {
235239
message: `${fetchError?.name ?? 'FetchError'}: ${fetchError?.message}`,
236240
details: errorDetails,
237-
hint: causeMessage ? `Underlying cause: ${causeMessage}` : '',
238-
code: errorCode,
241+
hint: '',
242+
code: '',
239243
},
240244
data: null,
241245
count: null,

packages/core/postgrest-js/test/fetch-errors.test.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { PostgrestClient } from '../src/index'
22
import { Database } from './types.override'
33

44
describe('Fetch error handling', () => {
5-
test('should bubble up DNS error code (ENOTFOUND or EAI_AGAIN) from fetch cause', async () => {
5+
test('should bubble up DNS error cause in details', async () => {
66
// Create a client with an invalid domain that will trigger DNS resolution error
77
const postgrest = new PostgrestClient<Database>(
88
'https://invalid-domain-that-does-not-exist.local'
@@ -15,21 +15,20 @@ describe('Fetch error handling', () => {
1515
expect(res.status).toBe(0)
1616
expect(res.statusText).toBe('')
1717

18-
// The error code should be a DNS error code from the cause
19-
// Different environments return different DNS error codes:
20-
// - ENOTFOUND: Domain doesn't exist (most common)
21-
// - EAI_AGAIN: Temporary DNS failure (common in CI)
22-
expect(['ENOTFOUND', 'EAI_AGAIN']).toContain(res.error!.code)
18+
// Client-side network errors don't populate code/hint (those are for upstream service errors)
19+
expect(res.error!.code).toBe('')
20+
expect(res.error!.hint).toBe('')
2321

24-
// The message should still contain the fetch error
22+
// The message should contain the fetch error
2523
expect(res.error!.message).toContain('fetch failed')
2624

27-
// The details should contain cause information
25+
// The details should contain cause information with error code
26+
// Different environments return different DNS error codes:
27+
// - ENOTFOUND: Domain doesn't exist (most common)
28+
// - EAI_AGAIN: Temporary DNS failure (common in CI)
2829
expect(res.error!.details).toContain('Caused by:')
29-
expect(res.error!.details).toMatch(/ENOTFOUND|EAI_AGAIN/)
30-
31-
// The hint should contain the underlying cause message with getaddrinfo
32-
expect(res.error!.hint).toContain('getaddrinfo')
30+
expect(res.error!.details).toContain('getaddrinfo')
31+
expect(res.error!.details).toMatch(/\(ENOTFOUND\)|\(EAI_AGAIN\)/)
3332
})
3433

3534
test('should handle network errors with custom fetch implementation', async () => {
@@ -52,12 +51,12 @@ describe('Fetch error handling', () => {
5251
const res = await postgrest.from('users').select()
5352

5453
expect(res.error).toBeTruthy()
55-
expect(res.error!.code).toBe('ENOTFOUND')
54+
expect(res.error!.code).toBe('')
55+
expect(res.error!.hint).toBe('')
5656
expect(res.error!.message).toBe('TypeError: fetch failed')
5757
expect(res.error!.details).toContain('Caused by:')
5858
expect(res.error!.details).toContain('getaddrinfo ENOTFOUND example.com')
59-
expect(res.error!.details).toContain('Error code: ENOTFOUND')
60-
expect(res.error!.hint).toContain('getaddrinfo ENOTFOUND example.com')
59+
expect(res.error!.details).toContain('(ENOTFOUND)')
6160
})
6261

6362
test('should handle connection refused errors', async () => {
@@ -81,9 +80,10 @@ describe('Fetch error handling', () => {
8180
const res = await postgrest.from('users').select()
8281

8382
expect(res.error).toBeTruthy()
84-
expect(res.error!.code).toBe('ECONNREFUSED')
83+
expect(res.error!.code).toBe('')
84+
expect(res.error!.hint).toBe('')
8585
expect(res.error!.details).toContain('connect ECONNREFUSED')
86-
expect(res.error!.hint).toContain('connect ECONNREFUSED')
86+
expect(res.error!.details).toContain('(ECONNREFUSED)')
8787
})
8888

8989
test('should handle timeout errors', async () => {
@@ -105,8 +105,10 @@ describe('Fetch error handling', () => {
105105
const res = await postgrest.from('users').select()
106106

107107
expect(res.error).toBeTruthy()
108-
expect(res.error!.code).toBe('ETIMEDOUT')
108+
expect(res.error!.code).toBe('')
109+
expect(res.error!.hint).toBe('')
109110
expect(res.error!.details).toContain('request timeout')
111+
expect(res.error!.details).toContain('(ETIMEDOUT)')
110112
})
111113

112114
test('should handle fetch errors without cause gracefully', async () => {
@@ -124,9 +126,11 @@ describe('Fetch error handling', () => {
124126
const res = await postgrest.from('users').select()
125127

126128
expect(res.error).toBeTruthy()
127-
expect(res.error!.code).toBe('FETCH_ERROR')
128-
expect(res.error!.message).toBe('TypeError: fetch failed')
129+
expect(res.error!.code).toBe('')
129130
expect(res.error!.hint).toBe('')
131+
expect(res.error!.message).toBe('TypeError: fetch failed')
132+
// When no cause, details should still have the stack trace
133+
expect(res.error!.details).toBeTruthy()
130134
})
131135

132136
test('should handle generic errors without code', async () => {
@@ -141,6 +145,7 @@ describe('Fetch error handling', () => {
141145

142146
expect(res.error).toBeTruthy()
143147
expect(res.error!.code).toBe('')
148+
expect(res.error!.hint).toBe('')
144149
expect(res.error!.message).toBe('Error: Something went wrong')
145150
})
146151

0 commit comments

Comments
 (0)