framework
framework copied to clipboard
useAsyncData does not pass errors from server to client-side
Environment
- Operating System:
Linux
- Node Version:
v14.18.1
- Nuxt Version:
3.0.0-27294839.7e5e50b
- Package Manager:
[email protected]
- Bundler:
Vite
Reproduction
https://stackblitz.com/edit/github-ygz9df?file=app.vue
Describe the bug
On server side the error content is rendered as expected while the client side says it's null.
Additional context
No response
Logs
No response
It now shows [Vue warn]: Hydration text content mismatch
https://stackblitz.com/edit/github-ygz9df-hnnre8?file=app.vue
@nndnha error
now provides just a Boolean value on client side (to prevent server details being unwittingly exposed) so you can v-if
it but not print out other details on client side.
If you need other details in the template you can assign them to a useState
.
@danielroe That's why we got the content mismatch
warning because it was transformed from FetchError
on server to Boolean
on client.
I think the security risk here is out of scope, it's not related to Nuxt or useAsyncData
. For example, we will get an FetchError
every time we do a $fetch request to https://jsonplaceholder.typicode.com/404 so if you want to hide that error then hide it on jsonplaceholder.typicode.com
as it is the root factor that exposes that error content, without Nuxt everyone still can get that error so how can it become Nuxt's problem? With 7e5e50bba7fe3bc564a7bf9fe72142b9e8e1062c we now can use useAsyncData
from any where on client side so I don't think it will make sense to hide error on hydration.
Can we assign an FetchError
instance to useState
? I think we should wait for #2076.
@nndnha See https://github.com/nuxt/framework/pull/2130#issuecomment-978008885 for the rationale.
If you feel you need the full error details on client side hydration, perhaps you could share your use case?
If you feel you need the full error details on client side hydration, perhaps you could share your use case?
For FetchError
, I want to get the http status code.
@nndnha I understand. One note: if you need to get the status code on client side and don't mind an extra request, you can also rerun the fetch.
const { error, refresh } = await useFetch('https://jsonplaceholder.typicode.com/404');
if (process.client && error.value) {
await refresh()
}
@danielroe Thanks for that solution, but @pi0 does care about the performance right? An extra duplicate request is a performance penalty for both server(internal server instead of jsonplaceholder.typicode.com
) and client.
I think both are valid points here. While error
reference should be accessible per env for logging purposes, It is not meant for being serialized to payload or using its content to render.
Standard usage would be: (note that error is used as a boolean -- hasError
)
<template>
<div v-if="data">{{ data }}</div>
<div v-else-if="error">
Error fetching data <button @click="refresh">retry</button>
</div>
<div v-else>Loading...</div>
</template>
<script setup>
const { pending, error, data, refresh } = await useFetch(
'https://jsonplaceholder.typicode.com/404'
);
</script>
BTW if you really want to expose message, you can explicitly leak it with another state:
<template>
<div v-if="data">{{ data }}</div>
<div v-else-if="error">Error fetching data: {{ fetchError }}</div>
<div v-else>Loading...</div>
</template>
<script setup>
const { data, error } = await useFetch('https://jsonplaceholder.typicode.com/404' );
const fetchError = useState('error', () => error.value.toString());
</script>
@danielroe BTW we might do better error hydration on the client-side, using a new Error instead of changing the type to boolean or probably better always make it boolean as hasError
and provide a callback for onError
handling by user this can also give a chance of implementing retry/fallback strategies easier.
@pi0 I'm not using the error content for rendering, I want to use its content to make some checks on rendering. I want to detect the response status code whenever it's 400, 404, 401, 403, 5xx... We won't retry on 4xx errors without alteration to the request parameters, won't we?
Fair point about retrying on specific errors. Thinking how we can improve _errors
then (into more general _state
) that can sync state between client and server + next step after hydration (retry or error)
@pi0 @danielroe In useAsyncData
we have transform
option to transform the result data, how about if we add another option called transformError
to alter the error? The default value of the transformError
option can be a function that returns a simple data likes (error) => error.toString()
or () => "An generic error message here..."
to reduce the payload size. Then in my case I will able to get my favorite status code by providing my custom transformError
:
<template>
<div v-if="data">{{ data }}</div>
<div v-else-if="error === 404">
Page not found
</div>
<div v-else>
Error fetching data <button @click="refresh">retry</button>
</div>
</template>
<script setup>
const { pending, error, data, refresh } = await useFetch(
'https://jsonplaceholder.typicode.com/404',
{
transformError: err => err.response.status
}
);
</script>
@pi0 @danielroe In
useAsyncData
we havetransform
option to transform the result data, how about if we add another option calledtransformError
to alter the error? The default value of thetransformError
option can be a function that returns a simple data likes(error) => error.toString()
or() => "An generic error message here..."
to reduce the payload size. Then in my case I will able to get my favorite status code by providing my customtransformError
:<template> <div v-if="data">{{ data }}</div> <div v-else-if="error === 404"> Page not found </div> <div v-else> Error fetching data <button @click="refresh">retry</button> </div> </template> <script setup> const { pending, error, data, refresh } = await useFetch( 'https://jsonplaceholder.typicode.com/404', { transformError: err => err.response.status } ); </script>
This would be a perfect way of solving the problem imo.
I'm experiencing the same issue as @nndnha, in that I want the status code on the client side. For me it's to show appropriate error UI, so that my users know exactly what's gone wrong.
My way of solving this until now was for some useFetchs (well, technically useLazyAsyncDatas, but who's counting) to only run on the client side, to ensure I have the full error context. In all cases, I was only ever looking at the status code anyway, and had a computed property to extract that from the rest of the context.
I think my ideal situation would be for the error property to always just be the error code, but I can see that there are cases where the rest of the error context could be useful, so being able to write a custom transformation strikes the perfect balance - offering full flexibility and ease of use, without making any assumptions for you.
I will also say that I appreciate Nuxt defaulting to hiding the error context because of the potential security issue it could cause - this is definitely the right approach to take for a default action (even if it does slightly hinder developer UX), and gives me great greater confidence that I'm not exposing myself to another issue somewhere else because I forgot to overwrite another default. Security first, additional functionality second.
After a bit of search trough the ohmyfetch (the library used by nuxtjs mentioned in the documentation) I obtained the status code easily, this is my solution:
<script setup lang="ts">
let errorStatus;
const { data: myData, error: error }: { data: any; error?: any } =
await useFetch(
"https://jsonplaceholder.typicode.com/404",
{
async onResponseError({ response }) { // onResponseError is a "ohmyfetch" method
errorStatus = response.status; // assign the error to the declared variable "errorStatus"
},
}
);
return { myData, error, errorStatus }; // returning all the variables
</script>
This is my first response to a GitHub issue, hope i wrote it at least understandable
I ran into the same issue trying to communicate statusCodes from data fetching performed on the server back to the client. I realized simply setting state on the server will lead to the desired result:
<script setup>
// pinia
import { useAppStore } from '@/store/app'
// assuming the api endpoint returns a 404 error using the sendError helper from h3
const { data, refresh, error } = await useAsyncData('key', () => $fetch('/api/endpoint'))
if (process.server && error?.value) {
const nuxtApp = useNuxtApp()
const appStore = useAppStore()
// patch nuxt state management
useState('statusCode', () => fetchError.value.response.status)
// set the response status code if you like
nuxtApp.ssrContext.event.res.statusCode = fetchError.value.response.status
// patch pinia
appStore.statusCode = fetchError.value.response.status
}
console.log(appStore.statusCode) // 404 on both server and client
console.log(useState('statusCode').value) // 404 on both server and client
</script>
I tweaked this solution a little so it can be reusable:
// ~/composables/useAsyncDataWithError.ts
import type {
AsyncData,
KeyOfRes,
PickFrom,
_Transform,
} from 'nuxt/dist/app/composables/asyncData'
import type { AsyncDataOptions, NuxtApp } from '#app'
export default async function useAsyncDataWithError<
DataT,
DataE = Error,
Transform extends _Transform<DataT> = _Transform<DataT, DataT>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
key: string,
handler: (ctx?: NuxtApp) => Promise<DataT>,
options: AsyncDataOptions<DataT, Transform, PickKeys> = {},
): Promise<AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE | null | true>> {
const serverError = useState<DataE | true | null>(`error-${key}`, () => null)
const { error, data, ...rest } = await useAsyncData(key, handler, options)
// Only set the value on server and if serverError is empty
if (process.server && error.value && !serverError.value)
serverError.value = error.value as DataE | true | null
// Clear error if data is available
if (data.value)
serverError.value = null
return {
...rest,
data,
error: serverError,
}
}
Then somewhere in your app
const { error } = useAsyncDataWithError('key', () => $fetch('https://jsonplaceholder.typicode.com/404'))
// error value is same on both server and client
There is already some way to easily get the body when response code is other than 200?
Another solution would be to change your backend server to return status 200 instead of 4xx for usage errors, that way you can read the error message/code from the payload and nothing private from the Nuxt server would be exposed.
I found another solution that will not only pass errors but also cookies. Assuming your server URL starts with /api
, create a file server/api/[...].ts
with the following content:
import { createError, useBody, appendHeader } from 'h3'
const config = useRuntimeConfig()
const baseURL = config.backendUrl
export default defineEventHandler(async event => {
const method = useMethod(event)
const params = useQuery(event)
const body = method === "GET" ? undefined : await useBody(event)
const { url, headers } = event.req
try {
const response = await $fetch.raw(url, {
headers: {
"Content-Type": "application/json",
cookie: headers.cookie
},
baseURL,
method,
params,
body
})
for (const header of ['set-cookie', 'cache-control']) {
if (response.headers.has(header)) {
appendHeader(event, header, response.headers.get(header));
}
}
return response._data
} catch (error) {
return createError({
statusCode: error.response.status,
statusMessage: error.message,
data: error.data,
});
}
})
Then add backendUrl
to nuxt.config.ts
:
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
runtimeConfig: {
// The private keys which are only available within server-side
backendUrl: 'http://localhost:5000',
// Keys within public, will be also exposed to the client-side
public: {
}
}
})
Another solution would be to change your backend server to return status 200 instead of 4xx for usage errors, that way you can read the error message/code from the payload and nothing private from the Nuxt server would be exposed.
No. Status codes are there for a reason. A serverside application returns a 404 for a reason: I cannot find what you are looking for. Give me the bloody error. I have no information with a boolean. I do not know if there is an internal server error, if a request is malformed, etc. Nuxt should not every decide for me that I am an absolute beginner thus that they need to hide useful error data.
Well-written rest API returns various error codes
Well-written rest API returns various error codes
And might include data in that response body. 404 is pretty self explanatory, but a 400 might include a message as to why your request is malformed.
It would be very helpful for me to have access to the error details. A request can fail for several reasons. Maybe the user is trying to access something that they haven't been authorized to access, in which case we might inform the user of this (and possibly suggest that they request access from the owner). Or maybe the user made a typo and they're (accidentally) trying to access something that doesn't exist, in which case we might tell the user to check for typos. Or maybe the server is down, in which case we might provide the user with a "Try Again" button.
If you use $fetch
then the error thrown is a FetchError
with all the relevant details in it. But it seems very odd that the useState
version of fetch has explicitly removed that information.
In my case, I used promises.
const { pending, error, data } = await useFetch(url, {
onResponse({ response }) {
return new Promise((resolve, reject) => {
response.ok ? resolve() : reject({ code: response.status, data: response.data })
})
}
})
if (error) {
// error is the value of the reject argument.
}
After a bit of search trough the ohmyfetch (the library used by nuxtjs mentioned in the documentation) I obtained the status code easily, this is my solution:
<script setup lang="ts"> let errorStatus; const { data: myData, error: error }: { data: any; error?: any } = await useFetch( "https://jsonplaceholder.typicode.com/404", { async onResponseError({ response }) { // onResponseError is a "ohmyfetch" method errorStatus = response.status; // assign the error to the declared variable "errorStatus" }, } ); return { myData, error, errorStatus }; // returning all the variables </script>
This is my first response to a GitHub issue, hope i wrote it at least understandable
I still don't understand why the nuxt developers won't share the error content with us, but I really liked your approach to this problem. I decided to use your code and thread on stackoverflow (https://stackoverflow.com/questions/72041740/how-to-set-global-api-baseurl-used-in-usefetch-in-nuxt-3) and it finally turned out to be something like this:
const onResponseError = async ({ response }: { response: FetchResponse<ApiError> }) => {
// Handle error
};
const testApi = async <T>(request: NitroFetchRequest, options: UseFetchOptions<T extends void ? unknown : T, (res: T extends void ? unknown : T) => T extends void ? unknown : T, KeyOfRes<(res: T extends void ? unknown : T) => T extends void ? unknown : T>>) => {
const statusCode = ref<number>(200);
const asyncData = await useFetch<T>(request, {
// Other common options
onResponseError: async (ctx) => {
statusCode.value = ctx.response.status;
await onResponseError(ctx);
},
...options,
});
return { ...asyncData, statusCode };
};
Theoretically, I could also overwrite the error
field this way, but I am able to handle any error with onResponseError
. For now, I will assume that the reason for the developers of nuxt is legitimate. I hope it will be useful to someone.