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

feat(useFetch): update data on success #2711

Merged
merged 4 commits into from Mar 14, 2023

Conversation

pbrzosko
Copy link
Contributor

Description

When people use useFetch, they expect certain type to be returned by the request, ie. an array of options to be provided to the select component. But this data is only present on successfull responses. In case of error, probably a totally different payload is returned. Having this payload set as a data, causes a lot of issues, because it is no longer what client of the useFetch expected.

Additional context

IMHO data in case of error could be provided as a separate state. But in this PR, data is set to initialData provided in options or null, when an error happens.
Payload, when error happened, is provided to the onFetchError callback, so client of the useFetch composable can do whatever is needed to transform provided payload to some meaningful error.


What is the purpose of this pull request?

  • Bug fix
  • New Feature
  • Documentation update
  • Other

Before submitting the PR, please make sure you do the following

  • Read the Contributing Guidelines.
  • Read the Pull Request Guidelines.
  • Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
  • Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123).
  • Ideally, include relevant tests that fail without this PR but pass with it.

@antfu antfu added the direction: approved The direction of feature/change is approved by the team. May require some small changes. label Feb 18, 2023
@antfu antfu added this to the 10.0 milestone Feb 18, 2023
@pbrzosko
Copy link
Contributor Author

Maintainers, what it your plan for this issue? I have created a PR that solves it: #2711. Can you either approve / disapprove?

@antfu antfu merged commit 78cfbdd into vueuse:main Mar 14, 2023
@@ -349,7 +349,7 @@ export function useFetch<T>(url: MaybeComputedRef<string>, ...args: any[]): UseF
const statusCode = ref<number | null>(null)
const response = shallowRef<Response | null>(null)
const error = shallowRef<any>(null)
const data = shallowRef<T | null>(initialData)
const data = shallowRef<T | null>(initialData || null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be careful, if initialData is falsy, it will be set as null, not expected.
E.g. if I set { initialData: false } as option, then data will be set to null instead of false. Is it possible to use ?? instead of ||?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - good catch. I will create another PR for this.

@aparajita
Copy link

FYI this is a BREAKING CHANGE and should have been documented as such. It is quite normal for an API to return error information along with an error status. Previously we could retrieve that response in fetchResult.data, but that code no longer works.

@aparajita
Copy link

Also note value of error goes completely unused, which means the return value of onFetchError has been rendered completely useless.

https://github.com/pbrzosko/vueuse/blob/358acd2c455020dfc7e108f14d7c80e2d2421096/packages/core/useFetch/index.ts#L463

@aparajita
Copy link

I honestly don't even see a way (other than terrible hacks) to get the response when an error status is returned. If so, this change was quite unfortunate.

@orkhanahmadov
Copy link

I agree with @aparajita. This is an undocumented breaking change that caused a lot of headaches for us. We downgraded @vueuse/core to v9 because of it.

Not sure why this change was even needed... In our use case request can receive validation errors with 422 status code and actual validation errors as payload. We receive the errors list and display to the user. Now with this change, firstly, it is broken, secondly we can no longer access to data for validation errors, it is always null on error... We need to implement workarounds, which I believe was not necessary at all... you could always access response.value.status to check if the request was successful or not, why modify data instead...

@antfu
Copy link
Member

antfu commented Apr 24, 2023

Open for PR to add a options to configure this behaviour

@aparajita
Copy link

@orkhanahmadov I worked around it by attaching the response data to a property on the error object passed to onFetchError.

@aparajita
Copy link

Open for PR to add a options to configure this behaviour

@antfu If you are willing to take a look, I will be happy to submit one.

@aparajita
Copy link

But in this PR, data is set to initialData provided in options or null, when an error happens.

No @pbrzosko, it isn't. You are only doing that in the .then of the fetch and not in the .catch.

It really would be nice if you would go back and fix this mess. You broke error handling for everyone who uses this function.

@antfu
Copy link
Member

antfu commented Apr 27, 2023

Sure, let's directly discuss in the PR. Thanks!

@pbrzosko
Copy link
Contributor Author

pbrzosko commented Apr 27, 2023

Also note value of error goes completely unused, which means the return value of onFetchError has been rendered completely useless.

https://github.com/pbrzosko/vueuse/blob/358acd2c455020dfc7e108f14d7c80e2d2421096/packages/core/useFetch/index.ts#L463

@aparajita Not true. Value of error is fetchError.message || fetchError.name or whatever you set it to be in onFetchError callback.

@pbrzosko
Copy link
Contributor Author

pbrzosko commented Apr 27, 2023

I honestly don't even see a way (other than terrible hacks) to get the response when an error status is returned. If so, this change was quite unfortunate.

@aparajita No hacks needed. You just need to set whatever error you want in onFetchError. The main reason for change was that if you expect to receive apples from an API, you want it to be apples, not oranges if there is an error. So apples (data) should. be in data ref, and error (oranges) should be in the error ref. Event data ref is typed to be T, but apparently sometimes it is something else. I don't think that is right behavior and thus this change. If you do not use typescript, but javascript only and expect anything to be there, maybe you do not have such an issue.
And in onFetchError you can get access to error response, parse it and assign to error ref if you want or you can set a custom error or whatever you like.

@pbrzosko
Copy link
Contributor Author

pbrzosko commented Apr 27, 2023

But in this PR, data is set to initialData provided in options or null, when an error happens.

No @pbrzosko, it isn't. You are only doing that in the .then of the fetch and not in the .catch.

It really would be nice if you would go back and fix this mess. You broke error handling for everyone who uses this function.

@aparajita I couldn't make it in the catch block, because then for a single moment data ref would hold the error (again not what it supposed to hold). But I agree, that it also should be set in catch block to the default value.

@pbrzosko
Copy link
Contributor Author

pbrzosko commented Apr 27, 2023

I agree with @aparajita. This is an undocumented breaking change that caused a lot of headaches for us. We downgraded @vueuse/core to v9 because of it.

Not sure why this change was even needed... In our use case request can receive validation errors with 422 status code and actual validation errors as payload. We receive the errors list and display to the user. Now with this change, firstly, it is broken, secondly we can no longer access to data for validation errors, it is always null on error... We need to implement workarounds, which I believe was not necessary at all... you could always access response.value.status to check if the request was successful or not, why modify data instead...

@orkhanahmadov And don't you think it is weird, that sometimes you have response type in data (T) and sometimes a list of errors?

@aparajita
Copy link

Also note value of error goes completely unused, which means the return value of onFetchError has been rendered completely useless.
https://github.com/pbrzosko/vueuse/blob/358acd2c455020dfc7e108f14d7c80e2d2421096/packages/core/useFetch/index.ts#L463

@aparajita Not true. Value of error is fetchError.message || fetchError.name or whatever you set it to be in onFetchError callback.

You are ignoring the possibility that the API might return an error status AND a response body to give more information about what the error was. fetchError is useless in such a case.

@orkhanahmadov
Copy link

@orkhanahmadov And don't you think it is weird, that sometimes you have response type in data (T) and sometimes a list of errors?

@pbrzosko I don't think I do. If the endpoint is returning data alongside a non-successful status code, I would assume I can also access that data easily just the same way I access the status code. Take my validation errors example: yes endpoint does return non-ok code, but it also returns important validation errors that I need to access and use. Unsetting the data on error only complicates the implementation of this.

@orkhanahmadov
Copy link

@pbrzosko now trying to implement onFetchError I found out it somehow breaks certain parts of useFetch. If I'm setting request headers and also using onFetchError it somehow ignores and drops defined headers.

import { useFetch } from '@vueuse/core'

useFetch('localhost:8081', {
  headers: { foo: 'bar' },
  onFetchError(ctx) { // defining `onFetchError` ignores `headers`. they won't get used
    return ctx
  }
})

useFetch('localhost:8081', {
  headers: { foo: 'bar' }, // without `onFetchError`, headers being used 
})

Am I missing something here? Seems like a bug. Currently with v10 if the endpoint returns a non-successful code with a body, seems like there's no way to access the body while also using headers.

I'll open a dedicated issue for this.

@aparajita
Copy link

@orkhanahmadov And don't you think it is weird, that sometimes you have response type in data (T) and sometimes a list of errors?

@pbrzosko I don't think I do. If the endpoint is returning data alongside a non-successful status code, I would assume I can also access that data easily just the same way I access the status code. Take my validation errors example: yes endpoint does return non-ok code, but it also returns important validation errors that I need to access and use. Unsetting the data on error only complicates the implementation of this.

Exactly my point. There has to be a mechanism to retrieve the body when there is an error status. I'm fine with keeping it separate from data, as that actually simplifies the typing, but there should be a clean way to get the response body when response.ok === false.

@danimalweb
Copy link

I've spent the last few hours puzzling over this myself. We have a 422 error being returned, and I need to access body data (validation)

Can anyone recommend a good pattern to work around this issue? Or should I simply downgrade?

@orkhanahmadov
Copy link

I've spent the last few hours puzzling over this myself. We have a 422 error being returned, and I need to access body data (validation)

Can anyone recommend a good pattern to work around this issue? Or should I simply downgrade?

@danimalweb We tried to implement onFetchError, but it seems to be broken if you use it with request headers. As described here and here. We downgraded to v9.

@aparajita
Copy link

I've spent the last few hours puzzling over this myself. We have a 422 error being returned, and I need to access body data (validation)

Can anyone recommend a good pattern to work around this issue? Or should I simply downgrade?

I do this:

interface APIError {
  error: number
  message: string
}

type ExtendedError = Error & { apiError?: APIError }

useFetch(url, {
    onFetchError(ctx: {
      data: string | APIError
      response: Response | null
      error: ExtendedError
    }) {
      // If the response is a string and is supposed to be JSON,
      // try to parse it as JSON. We need to do this because many of our
      // endpoints return a success status and no body on success, and
      // an error status and JSON error information in the body. If you try to
      // use .json() on an empty body, useFetch throws an exception and breaks.

      const contentType = ctx.response?.headers.get('content-type') ?? ''

      if (contentType.startsWith('application/json')) {
        if (typeof ctx.data === 'string') {
          try {
            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- We're parsing JSON.
            ctx.error.apiError = JSON.parse(ctx.data)
          } catch {
            // Ignore
          }
        } else {
          // Already parsed, save it
          ctx.error.apiError = ctx.data
        }
      }

      return ctx
    },

The error that is then passed to fetchResult.onFetchError will have a .apiError property with the contents of the response body if there was an error.

@climba03003
Copy link
Contributor

climba03003 commented May 12, 2023

Finally found why 10.1.2 failed.
The intention for onFetchError is centralize data handling.
See #841 (comment)

Fetch is treating 400-500 status code as error even when the API is returning the data we expect.
So, it is the only hook (not right now) that allow developer to pass the correct data to data.value.

error and data should not be determine by fetch or you.
It should be determine by the developer, this PR is forcing people to follow the contributor expectation (fetch error can only be error) and eliminate the ability for developer to do their expectation (allow data even in fetch error).
If he / she do not want the data in fetch error, just don't return data.

And yes, it is actually shipping breaking change as bugfix and also ruins the API expectation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
direction: approved The direction of feature/change is approved by the team. May require some small changes.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants