Skip to content

Commit a2642c3

Browse files
feat: pass-through status code, messages etc.
1 parent 912c9f3 commit a2642c3

File tree

2 files changed

+106
-48
lines changed

2 files changed

+106
-48
lines changed

docs/guide/how-it-works.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
The internal `/api/__kirby__` server route proxies KQL requests to the Kirby instance.
66

7+
::: tip
8+
The proxy layer will not only pass through your API's response body to the response on the client, but also HTTP status code, HTTP status message and headers. This way, you can handle errors just like you would with a direct API call.
9+
:::
10+
711
## Detailed Answer
812

913
The [`useKql`](/api/use-kql) and [`$kql`](/api/kql) composables will initiate a POST request to the Nuxt server route `/api/__kirby__` defined by this module. The KQL query will be encoded in the request body.

src/runtime/server/handler.ts

Lines changed: 102 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { joinURL } from 'ufo'
21
import { consola } from 'consola'
3-
import { createError, defineEventHandler, getRouterParam, readBody } from 'h3'
2+
import { createError, defineEventHandler, getRouterParam, readBody, send, setResponseHeader, setResponseStatus, splitCookiesString } from 'h3'
43
import type { ModuleOptions } from '../../module'
54
import { createAuthHeader } from '../utils'
65
import type { ServerFetchOptions } from '../types'
7-
import type { NuxtError } from '#app'
86

97
// @ts-expect-error: Will be resolved by Nitro
108
import { defineCachedFunction } from '#internal/nitro'
@@ -22,52 +20,40 @@ async function fetcher({
2220
}: { key: string } & ServerFetchOptions) {
2321
const isQueryRequest = key.startsWith('$kql')
2422

25-
try {
26-
const result = await globalThis.$fetch<any>(isQueryRequest ? kql.prefix : path!, {
27-
baseURL: kql.url,
28-
...(isQueryRequest
29-
? {
30-
method: 'POST',
31-
body: query,
32-
}
33-
: {
34-
query,
35-
method,
36-
body,
37-
}),
38-
headers: {
39-
...headers,
40-
...createAuthHeader(kql),
41-
},
42-
})
23+
const response = await globalThis.$fetch.raw<ArrayBuffer>(isQueryRequest ? kql.prefix : path!, {
24+
responseType: 'arrayBuffer',
25+
ignoreResponseError: true,
26+
baseURL: kql.url,
27+
...(isQueryRequest
28+
? {
29+
method: 'POST',
30+
body: query,
31+
}
32+
: {
33+
query,
34+
method,
35+
body,
36+
}),
37+
headers: {
38+
...headers,
39+
...createAuthHeader(kql),
40+
},
41+
})
4342

44-
return result
45-
}
46-
catch (error) {
47-
if (isQueryRequest) {
48-
consola.error(
49-
`KQL query failed with status code ${(error as NuxtError).statusCode}:\n`,
50-
JSON.stringify((error as NuxtError).data, undefined, 2),
51-
)
52-
if (kql.server.verboseErrors)
53-
consola.log('KQL query request:', query)
54-
}
55-
else {
56-
consola.error(`Failed ${(method || 'get')?.toUpperCase()} request to ${joinURL(kql.url, path!)} with options:`, { headers, query, body })
57-
}
43+
// Serialize the response data
44+
const buffer = Buffer.from(response._data ?? ([] as unknown as ArrayBuffer))
45+
const data = buffer.toString('base64')
5846

59-
throw createError({
60-
statusCode: 500,
61-
statusMessage: isQueryRequest
62-
? 'Failed to execute KQL query'
63-
: `Failed to fetch "${path}"`,
64-
data: (error as NuxtError).statusMessage,
65-
})
47+
return {
48+
status: response.status,
49+
statusText: response.statusText,
50+
headers: [...response.headers.entries()],
51+
data,
6652
}
6753
}
6854

6955
const cachedFetcher = defineCachedFunction(fetcher, {
70-
name: 'kql-fetcher',
56+
name: 'kirby',
7157
base: kql.server.storage,
7258
swr: kql.server.swr,
7359
maxAge: kql.server.maxAge,
@@ -77,12 +63,13 @@ const cachedFetcher = defineCachedFunction(fetcher, {
7763
export default defineEventHandler(async (event) => {
7864
const body = await readBody<ServerFetchOptions>(event)
7965
const key = decodeURIComponent(getRouterParam(event, 'key')!)
66+
const isQueryRequest = key.startsWith('$kql')
8067

81-
if (key.startsWith('$kql')) {
68+
if (isQueryRequest) {
8269
if (!body.query?.query) {
8370
throw createError({
8471
statusCode: 400,
85-
statusMessage: 'Empty KQL query',
72+
statusMessage: 'KQL query is empty',
8673
})
8774
}
8875
}
@@ -96,8 +83,75 @@ export default defineEventHandler(async (event) => {
9683
}
9784
}
9885

99-
if (kql.server.cache && body.cache)
100-
return await cachedFetcher({ key, ...body })
86+
let response: Awaited<ReturnType<typeof fetcher>>
87+
const queryErrorMessage = `Failed KQL query "${body.query?.query}" (...)`
88+
const fetchErrorMessage = `Failed ${(body.method || 'get')?.toUpperCase()} request to "${body.path!}"`
89+
90+
try {
91+
response = kql.server.cache && body.cache
92+
? await cachedFetcher({ key, ...body })
93+
: await fetcher({ key, ...body })
94+
}
95+
catch (error) {
96+
consola.error(error)
97+
98+
throw createError({
99+
statusCode: 500,
100+
statusMessage: isQueryRequest
101+
? queryErrorMessage
102+
: fetchErrorMessage,
103+
})
104+
}
101105

102-
return await fetcher({ key, ...body })
106+
if (response.status >= 400 && response.status < 600) {
107+
if (isQueryRequest) {
108+
consola.error(`${queryErrorMessage} with status code ${response.status}:\n`, tryParseJSON(response.data))
109+
if (kql.server.verboseErrors)
110+
consola.log('Full KQL query request:', body.query)
111+
}
112+
else {
113+
consola.error(fetchErrorMessage)
114+
}
115+
116+
throw createError({
117+
statusCode: 500,
118+
statusMessage: isQueryRequest
119+
? queryErrorMessage
120+
: fetchErrorMessage,
121+
data: tryParseJSON(response.data),
122+
})
123+
}
124+
125+
const cookies: string[] = []
126+
127+
for (const [key, value] of response.headers) {
128+
if (key === 'content-encoding')
129+
continue
130+
131+
if (key === 'content-length')
132+
continue
133+
134+
if (key === 'set-cookie') {
135+
cookies.push(...splitCookiesString(value))
136+
continue
137+
}
138+
139+
setResponseHeader(event, key, value)
140+
}
141+
142+
if (cookies.length > 0)
143+
setResponseHeader(event, 'set-cookie', cookies)
144+
145+
const buffer = Buffer.from(response.data, 'base64')
146+
setResponseStatus(event, response.status, response.statusText)
147+
return send(event, buffer)
103148
})
149+
150+
function tryParseJSON(data: string) {
151+
try {
152+
return JSON.parse(data)
153+
}
154+
catch (e) {
155+
return data
156+
}
157+
}

0 commit comments

Comments
 (0)