Skip to content

Commit

Permalink
feat!: pass-through response status and headers to client (#44)
Browse files Browse the repository at this point in the history
* feat: pass-through response headers to client
Breaking Change
Resolves #25 and #41

* chore: fix tests

* fix: Add missing imports for h3 utilities

* test: add blob test

* docs: update error handling guide

* chore: fix linting issue

---------

Co-authored-by: Johann Schopplich <[email protected]>
  • Loading branch information
mattmess1221 and johannschopplich authored Aug 31, 2023
1 parent 650fc44 commit 5a76bda
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 123 deletions.
7 changes: 1 addition & 6 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
items: [
{ text: 'Getting Started', link: '/guide/getting-started' },
{ text: 'How It Works', link: '/guide/how-it-works' },
{ text: 'Error Handling', link: '/guide/error-handling' },
],
},
{
Expand All @@ -149,12 +150,6 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
{ text: 'OpenAPI Types', link: '/guide/openapi-types' },
],
},
{
text: 'FAQ',
items: [
{ text: 'How to Track Errors', link: '/guide/faq-how-to-track-errors' },
],
},
{ text: 'Migration', link: '/guide/migration' },
{ text: 'API', link: '/api/' },
{ text: 'Playground', link: 'https://github.com/johannschopplich/nuxt-api-party/tree/main/playground' },
Expand Down
94 changes: 94 additions & 0 deletions docs/guide/error-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Error Handling

While the idea of this Nuxt module is to mask your real API (and credentials) by creating a server proxy, `nuxt-api-party` will minimize the hassle of handling errors by passing the following properties to the client:

- Response body
- HTTP status code
- HTTP status message
- Headers

Thus, if your API fails to deliver, you can still handle the error response in your Nuxt app just like you would with a direct API call.

Both [generated composables](/api/) per endpoint will throw an [ofetch](https://github.com/unjs/ofetch) `FetchError` if your API fails to deliver.

Logging the `error.data` property will provide you with the response body, like:

```json
{
"message": "Not Found",
"statusCode": 404,
"statusMessage": "Not Found",
"url": "/api/foo/bar"
}
```

See all available examples below.

## `FetchError` Type Declaration

```ts
// See https://github.com/unjs/ofetch
interface FetchError<T = any> extends Error {
request?: FetchRequest
options?: FetchOptions
response?: FetchResponse<T>
data?: T
status?: number
statusText?: string
statusCode?: number
statusMessage?: string
}
```

## Examples

::: info
The examples below assume that you have set up an API endpoint called `jsonPlaceholder`:

```ts
// `nuxt.config.ts`
export default defineNuxtConfig({
modules: ['nuxt-api-party'],

apiParty: {
endpoints: {
jsonPlaceholder: {
url: 'https://jsonplaceholder.typicode.com'
}
}
}
})
```

:::

### Usage with `useJsonPlaceholderData`

When using the `useMyApiData` composable, the `error` is already typed as a `FetchError`.

```ts
const { data, error } = await useJsonPlaceholderData('not/available')

watchEffect(() => {
if (error.value)
console.error(error.data)
})
```

### Usage with `$jsonPlaceholder`

```ts
import type { FetchError } from 'ofetch'

function onSubmit() {
try {
const response = await $jsonPlaceholder('not/available', {
method: 'POST',
body: form.value
})
}
catch (error) {
console.error((error as FetchError).data)
}
}
```
75 changes: 0 additions & 75 deletions docs/guide/faq-how-to-track-errors.md

This file was deleted.

19 changes: 18 additions & 1 deletion docs/guide/migration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Migration

## v0.17.0

::: tip
The breaking changes only apply, if you rely on error handling with your API composables.
:::

With this version, the API response including status code and headers will be passed to the client fetch call. As such, the properties like `statusCode` and `statusMessage` of the error object contain the response status code and message, respectively. Before v0.17.0, these properties were always returning 404 and "Not Found" respectively.

The response body is still available via the `data` property of the error object:

```ts
import type { FetchError } from 'ofetch'

// Log your API's error response
console.error('Error response body:', (error as FetchError).data)
```

## v0.10.0

::: tip
Expand Down Expand Up @@ -30,7 +47,7 @@ export default defineNuxtConfig({

If you are using the following environment variables in your project's `.env` file:

```bash
```ini
API_PARTY_BASE_URL=your-api-url
# Optionally, add a bearer token
API_PARTY_TOKEN=your-api-token
Expand Down
23 changes: 16 additions & 7 deletions src/runtime/server/handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { createError, defineEventHandler, getRequestHeader, getRouterParam, readBody } from 'h3'
import type { FetchError } from 'ofetch'
import { createError, defineEventHandler, getRequestHeader, getRouterParam, readBody, removeResponseHeader, send, setResponseHeaders, setResponseStatus } from 'h3'
import { deserializeMaybeEncodedBody } from '../utils'
import type { ModuleOptions } from '../../module'
import type { EndpointFetchOptions } from '../utils'
Expand Down Expand Up @@ -52,7 +51,7 @@ export default defineEventHandler(async (event): Promise<any> => {
}

try {
return await globalThis.$fetch(
const response = await globalThis.$fetch.raw<ArrayBuffer>(
path,
{
...fetchOptions,
Expand All @@ -68,16 +67,26 @@ export default defineEventHandler(async (event): Promise<any> => {
...headers,
},
...(body && { body: await deserializeMaybeEncodedBody(body) }),
ignoreResponseError: true,
responseType: 'arrayBuffer',
},
)
setResponseStatus(event, response.status, response.statusText)
setResponseHeaders(event, Object.fromEntries(response.headers.entries()))

// ofetch has already decoded the response. Leaving this header can cause the
// client issues when decoding and may create a conflict if a compression
// middleware is used
removeResponseHeader(event, 'content-encoding')

await send(event, new Uint8Array(response._data ?? []))
}
catch (error) {
const { response } = error as FetchError
console.error(error)

throw createError({
statusCode: response?.status,
statusMessage: response?.statusText,
data: response?._data,
statusCode: 503,
statusMessage: 'Service Unavailable',
})
}
})
53 changes: 26 additions & 27 deletions test/__snapshots__/e2e.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`nuxt-api-party > fetches data with $testApi 1`] = `
[
{
"completed": false,
"id": 1,
"title": "delectus aut autem",
"userId": 1,
},
{
"completed": false,
"id": 2,
"title": "quis ut nam facilis et officia qui",
"userId": 1,
},
{
"completed": false,
"id": 3,
"title": "fugiat veniam minus",
"userId": 1,
},
]
{
"blob": "Foo",
"json": [
{
"completed": false,
"id": 1,
"title": "delectus aut autem",
"userId": 1,
},
{
"completed": false,
"id": 2,
"title": "quis ut nam facilis et officia qui",
"userId": 1,
},
{
"completed": false,
"id": 3,
"title": "fugiat veniam minus",
"userId": 1,
},
],
}
`;

exports[`nuxt-api-party > fetches data with useTestApiData 1`] = `
Expand Down Expand Up @@ -49,19 +52,15 @@ exports[`nuxt-api-party > fetches data with useTestApiData 1`] = `
]
`;

exports[`nuxt-api-party > throws error for invalid response 1`] = `
exports[`nuxt-api-party > throws error for invalid response with $testApi 1`] = `
{
"data": {
"message": "Not Found",
"stack": "",
"statusCode": 404,
"statusMessage": "Not Found",
"url": "/api/not-found",
"reason": "anything",
},
"message": "Not Found",
"stack": "",
"statusCode": 404,
"statusMessage": "Not Found",
"url": "/api/__api_party/testApi",
"url": "/api/foo/bar",
}
`;
8 changes: 4 additions & 4 deletions test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ describe('nuxt-api-party', async () => {
expect(getTestResult(html)).toMatchSnapshot()
})

it('fetches data with useTestApiData', async () => {
const html = await $fetch('/useTestApiData')
it('throws error for invalid response with $testApi', async () => {
const html = await $fetch('/$testApi-error')
expect(getTestResult(html)).toMatchSnapshot()
})

it('throws error for invalid response', async () => {
const html = await $fetch('/invalid')
it('fetches data with useTestApiData', async () => {
const html = await $fetch('/useTestApiData')
expect(getTestResult(html)).toMatchSnapshot()
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { FetchError } from 'ofetch'
try {
await $testApi('not-found', {
await $testApi('foo/bar', {
headers: {
accept: 'application/json',
},
Expand Down
8 changes: 6 additions & 2 deletions test/fixture/pages/$testApi.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<script setup lang="ts">
import type { TestApiTodo } from '~/types'
const data = await $testApi<TestApiTodo>('todos')
const json = await $testApi<TestApiTodo>('todos')
const blob = await $testApi('blob')
useTestResult(data)
useTestResult({
json,
blob,
})
</script>
3 changes: 3 additions & 0 deletions test/fixture/server/api/[...].ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export default defineEventHandler(() => {
throw createError({
data: {
reason: 'anything',
},
statusCode: 404,
statusMessage: 'Not Found',
})
Expand Down
3 changes: 3 additions & 0 deletions test/fixture/server/api/blob.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default defineEventHandler(() => {
return new Blob(['Foo'], { type: 'text/plain' })
})

0 comments on commit 5a76bda

Please sign in to comment.