Skip to content
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

useAsyncData does not pass errors from server to client-side #12885

Closed
nndnha opened this issue Nov 24, 2021 · 26 comments · Fixed by nuxt/framework#2130 or nuxt/framework#8521
Closed

useAsyncData does not pass errors from server to client-side #12885

nndnha opened this issue Nov 24, 2021 · 26 comments · Fixed by nuxt/framework#2130 or nuxt/framework#8521
Assignees

Comments

@nndnha
Copy link

nndnha commented Nov 24, 2021

Environment

  • Operating System: Linux
  • Node Version: v14.18.1
  • Nuxt Version: 3.0.0-27294839.7e5e50b
  • Package Manager: yarn@1.22.15
  • 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

@danielroe danielroe changed the title useAsyncData hydration mismatch useAsyncData does not pass errors from server to client-side Nov 24, 2021
@danielroe danielroe self-assigned this Nov 24, 2021
@nndnha
Copy link
Author

nndnha commented Nov 25, 2021

It now shows [Vue warn]: Hydration text content mismatch

https://stackblitz.com/edit/github-ygz9df-hnnre8?file=app.vue

@danielroe danielroe reopened this Nov 25, 2021
@danielroe
Copy link
Member

danielroe commented Nov 25, 2021

@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.

@nndnha
Copy link
Author

nndnha commented Nov 25, 2021

@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 7e5e50b 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 #12831.

@danielroe
Copy link
Member

@nndnha See nuxt/framework#2130 (comment) for the rationale.

If you feel you need the full error details on client side hydration, perhaps you could share your use case?

@nndnha
Copy link
Author

nndnha commented Nov 25, 2021

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.

@danielroe
Copy link
Member

@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()
}

@nndnha
Copy link
Author

nndnha commented Nov 25, 2021

@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.

@pi0
Copy link
Member

pi0 commented Nov 25, 2021

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.

@nndnha
Copy link
Author

nndnha commented Nov 25, 2021

@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?

@pi0
Copy link
Member

pi0 commented Nov 25, 2021

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)

@nndnha
Copy link
Author

nndnha commented Nov 28, 2021

@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>

@matthewpull
Copy link

matthewpull commented Jan 4, 2022

@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>

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.

@RiccardoCherchi
Copy link

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

@j0Shi82
Copy link

j0Shi82 commented May 8, 2022

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>

@wobsoriano
Copy link
Member

wobsoriano commented May 19, 2022

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

@gitFoxCode
Copy link

There is already some way to easily get the body when response code is other than 200?

@krisfris
Copy link

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.

@krisfris
Copy link

krisfris commented Aug 18, 2022

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: {
    }
  }
})

@MueR
Copy link

MueR commented Sep 28, 2022

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.

@gitFoxCode
Copy link

Well-written rest API returns various error codes

@MueR
Copy link

MueR commented Sep 28, 2022

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.

@ataylor32
Copy link

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.

@abhay-agarwal
Copy link

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.

@alfa-jpn
Copy link

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.
}

@szulcus
Copy link

szulcus commented Oct 26, 2022

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.

@manuel-84
Copy link

anyone getting how to solve the same issue with onRequestError ?
seems there is no way to get response status with this handler, for example when the server return a 401

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet