Open danbahrami opened 4 months ago
Let's start by adding { request, response, options }
as the second argument to parseJson
. My only concern with that is users may already be passing in a JSON.stringify
-like function with multiple arguments that are incompatible with this new signature (i.e. the Array.map + parseInt footgun). It would be easy to update by wrapping the parser in an arrow function, though. We'll just need to mention this in the release notes.
Regarding network errors, indeed a lot of people have requested similar functionality. A major version bump is no problem, one is likely coming soon anyway because of PR #602.
However, we are not yet in a great place to provide this feature for a few reasons:
fetch()
doesn't throw discrete error types for network errors vs other errors. In fact, there's nothing at all to even indicate an error came from fetch
, other than some hardcoded assumptions about error messages. The error messages and properties also vary between environments. We can and should use is-network-error to make a best-effort attempt at this, but just be aware I doubt every edge case is handled correctly or consistently. For example, is-network-error#4 (comment) indicates that, in Chrome but not Safari, an aborted request looks like a network error and therefor is treated as such by is-network-error
. This is really a spec-level problem and there hasn't been much movement from WHATWG to solve it.ky()
to throw them. That's all fine and good except then there is no Response
or anything for us to return and you run into the same problem Sindre mentioned in #508 (comment). Your code will end up erroring on its own because it expects success if we don't throw. The only sensible solution I can think of is #508 (comment), but that's a big change, and I suspect it would be painful for TypeScript users.All that being said, I will happily review any PR you send. Just don't be surprised if we decide not to move forward with it. 🙂
@sholladay thanks for the super fast response 🙇 Couple of thoughts on your points:
- fetch() doesn't throw discrete error types for network errors vs other errors. In fact, there's nothing at all to even indicate an error came from fetch, other than some hardcoded assumptions about error messages. The error messages and properties also vary between environments. We can and should use is-network-error to make a best-effort attempt at this, but just be aware I doubt every edge case is handled correctly or consistently. For example, https://github.com/sindresorhus/is-network-error/issues/4#issuecomment-1940520968 indicates that, in Chrome but not Safari, an aborted request looks like a network error and therefor is treated as such by is-network-error. This is really a https://github.com/whatwg/fetch/issues/526 and there hasn't been much movement from WHATWG to solve it.
Based on this it feels like trying to read into what went wrong when fetch()
throws (e.g. is this a network error?) is outside the responsibility of ky
. What makes this a great library, as opposed to something like axios
, is that it's a relatively thin wrapper around fetch()
. If you were handling this error with fetch()
it might look something like...
try {
const response = await fetch(url);
} catch (e) { // e is typed to any or unknown based on TS config
if (e instanceof TypeError) {
// probably a network error
} else if (e instanceof Error && e.name === "AbortError") {
// client aborted
} else if (e instanceof Error) {
// some random unknown error
} else {
// ok something really strange has happened
}
}
I think it's fair to expect consumers to perform similar error handling with ky
. A more honest name might be onFetchError
rather than onNetworkError
.
type BeforeNetworkErrorState = {
request: Request;
options: NormalizedOptions;
error: unknown; // type as unknown so consumers can do their own type narrowing
};
const onFetchError: OnFetchErrorHook = ({ error, request, response, options }) => {
if (error instanceof TypeError) {
// probably a network error
} else if (error instanceof Error && error.name === "AbortError") {
// client aborted
} else if (error instanceof Error) {
// some random unknown error
} else {
// ok something really strange has happened
}
}
Consumers are able to use is-network-error
if they choose. Or at a later point we could do it internally and expose a NetworkError
type like we do for HTTPError
which consumers could use for type-narrowing.
- Presumably, if you handle network errors via a hook, then you don't want ky() to throw them. That's all fine and good except then there is no Response or anything for us to return and you run into the same problem Sindre mentioned in https://github.com/sindresorhus/ky/issues/508#issuecomment-1568281797. Your code will end up erroring on its own because it expects success if we don't throw. The only sensible solution I can think of is https://github.com/sindresorhus/ky/issues/508#issuecomment-2190125228, but that's a big change, and I suspect it would be painful for TypeScript users.
I agree that it's not a good idea to allow handling of fetch errors because it would cause problems upstream. Perhaps having a naming convention of the hooks could set expectations about that:
beforeFoo
runs before an event and allows the input to be modifiedafterFoo
runs after an event and allows the output to be modifiedonFoo
runs after an event but is only to observe, doesn't modify anythingSo in this case onFetchError
would not allow you to handle errors, only observe.
You're welcome!
Based on this it feels like trying to read into what went wrong when fetch() throws (e.g. is this a network error?) is outside the responsibility of ky
There's definitely a line to be drawn somewhere there. I think its perfectly reasonable to expect Ky to wallpaper over some of fetch
's deficiencies and make the general task of error handling easier. But expecting Ky to solve the inconsistent handling of AbortError
s might be a bit too far.
we could do it internally and expose a NetworkError type like we do for HTTPError
That's what I want to do. We'll expose the fetch
error as the NetworkError
's cause
, which will enable users who need something narrower than our definition of a network error to at least make an attempt at distinguishing them.
So in this case onFetchError would not allow you to handle errors, only observe.
That sounds pretty reasonable. We could even allow users to return
a modified error if they want to, which will then be thrown after the hook. But then, does this behavior actually solve the "centralized logging" use case cleanly? I mean, surely any decent logging system is going to be monitoring errors thrown by Ky. If it's also logging inside of a hook, then you will likely end up with duplicate error logs. It seems kind of self-defeating to me. But I still think it could be useful for enriching errors with metadata.
surely any decent logging system is going to be monitoring errors thrown by Ky. If it's also logging inside of a hook, then you will likely end up with duplicate error logs.
In my case I'd like to use a shared api client across multiple different apps. Sometimes directly making fetch requests, sometimes inside React Query where errors are caught automatically. I'd like to have one consistent way of logging API errors everywhere. To me it makes sense to do it centrally otherwise it'll be hard to maintain consistent logging everywhere.
This discussion is making me wonder if perhaps the problem of logging is different to what ky
's hooks are for. Do you think there would be appetite to add something completely new to the config like a logger
function?
It could take a stream of events that are typed as a discriminated union so you could add different events over time without breaking changes.
type LogEvent =
{ type: "RequestStart"; request: Request; options: Options }
| { type: "ResponseSuccess"; request: Request; response: Response; options: Options }
| { type: "ResponseError"; request: Request; error: Error; options: Options }
| { type: "JsonParseError"; request: Request; error: Error; options: Options };
type Logger = (event: LogEvent) => void;
I like that API. It might have too much overlap with hooks for both APIs to exist, but I'll give it some thought.
For now, let's create the NetworkError
and ParseError
classes and route them through the onError
hook.
Hi! I'd like to contribute to
ky
for selfish reasons. I want to use it in my org's production app however, after trying to hook up the necessary logging we need I've come to a dead end.The logging that I need is:
beforeRequest
hook works for this)afterResponse
works for success and HTTPError responses but not Network Errors)Response.json()
failsSo that leaves me with two gaps:
Response.json()
errorsI'd like to get a rough steer on an approach for these both before creating a PR.
Hooking into
Response.json()
errorsMy first attempt at this was to try to do it in my own
parseJson
method:The problem with this approach is that, within the context of
parseJson
you don't have access to the request context so you can't send any information about the specific request along with the error.Possible solutions
I can think of two sensible options:
1. Pass the request information to the
parseJson
option This would look something like:not sure exactly what you'd want to pass in that second argument, could potentially contain the response as well if that's feasible.
2. A new hook -
beforeJsonParseError
This would be more fitting with how we can hook into HTTP Errors.Let me know if either of those sound reasonable or if you have any other ideas.
Hooking into network errors
This is already covered by another issue: https://github.com/sindresorhus/ky/issues/296
With this one I'm not sure if it's worth creating a new hook or expanding the
beforeError
hook to also run on network errors. ModifyingbeforeError
behaviour would probably require a major version bump as it could be a breaking change but it does feel like the more sensible API.otherwise I could add a new
beforeNetworkError
hookAgain, please let me know if either of those sound reasonable or if you have any other ideas.
Thanks!