-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
[RFC] Split HTTP::Client::Response in two #9901
Comments
I was having the same thought process for it actually, so I'm all for it. If wanted The biggest breakage here will be people typing stuff as The alternative to subclassing is making Third option I see would be to make |
Another idea: let both have
But maybe the downsides are not that bad. And we could keep a single But I still feel having two types is better. |
Yeah, that seems like it's equally brittle to use as the current interface. I would prefer forcing users to always call |
Yeah, this part of the API is really shady and I'm all in for improving that. I'm not to sure about having two separate types, though. Feels a bit overhead and IMO might not be that user-friendly. There's another solution I've already mentioned in #8461 (comment): Yield the IO as a second argument to the block in the streaming scenario. I'd like to inspect some real examples to see how this is used in the field. That should help us discuss an optimal improvement for this API. |
Hm, yielding it as second argument still seems to have potential runtime errors or weird behavior when mixing using that IO with calling |
According to skimming the code available on Github, by far the most common scenario is a quick and dirty call which doesn't really need a response object and just retrieves the body as a string: HTTP::Client.get("http://example.com").body (more elaborate versions also include a Maybe there should be a convenience method for this which also handles some HTTP protocol context (like status code). I think that might really be a great solution. Essentially, it just means removing |
Why there is one that consumes the body to a string, and not the other way around? It's not obvious at a first glance, so I think having a single behavior for both will be less confusing. We could have: getter body_io : IO
# Consummes the body IO
def body!
@body_io.gets_to_end
end |
That's #9901 (comment) with different names, so 👎 |
If we want to do something for 1.0 I am inclined to:
That way:
If we want to tweak further the design I think is after 1.0. |
The one-liner argument is not a good enough one, because we can do: p HTTP::Client.get("http://example.com").body
p HTTP::Client.get "http://example.com", &.body_io.gets_to_end
# Perhaps even p HTTP::Client.get "http://example.com", &.body! |
@j8r I don't understand your last comment. What point are you trying to make? Is it hat the yielding variant is only marginally longer and the "simple" one isn't necessary? |
Yes, I mean that all could be streaming, and as user-friendly as non-streaming. Both kind of users - those needing more low level/performance and those just wanting something easy - will be happy then. |
I'm not sure whether that really improves the situation, it just seems to shift the inconsistency away from the particular case that triggered this discussion, so I think I would then rather prefer to leave things as is. (Personally I think 1.0 is the perfect opportunity for a breaking change like this and starting with a clean API into the stable phase). To me this is about getting rid of having both |
With this RFC I'm trying to avoid runtime errors and move them to compile-time errors. If the most common case is fetching a response without a block and getting the body as a string then it shouldn't matter if we introduce two response types, because most don't have to deal with a specific type name. The |
But what's the point in having two separate types when one of them is rarely used and can simply be emulated using the other one? |
Type safety. |
I know that's the goal for this RFC. My suggestion to remove |
I think in most cases you want a non-streaming response because responses are small. However, in a few cases you'd like a streaming response, for example when downloading a large file, or when the response is meant to be streaming (like sending lines of JSON that you parse individually, and the connection is kept open). The way we design it, for the streaming way you have to do it in a block so you can't forget to close the IO. In Go you always get an "IO" and you have to remember to close it, and we didn't like that. So, we have two use cases and the way you use them in the same except that in one case you deal with a String, in the other you deal with an IO. We could extract that outside of the response, body = HTTP::Client.get("...") When you talk about an HTTP response body, the body really belongs to the response, it's not a separate entity. And it's just nice to be able to pass the response to other functions. What I'm wondering is: if we split it into two types, in which way that produces friction? |
I feel like the most simple breaking change we could do right now is removing |
I totally agree to your assessment of the typical use cases. I already mentioned that in #9901 (comment) HTTP::Client.get("http://example.com") # returns String
HTTP::Client.get("http://example.com") do |response|
# here we consume response body io
end If you just want a simple response body, you use the non-yielding method and get a string result. The method would also provide basic status code handling (for example raise or return If you have more advanced use case, like responding to specific status codes, handling response headers etc. you use the yielding method and get a response object (which only provides a streaming IO). If you need the body as string, you have to consume the IO. |
Then the vast majority of use cases are wrong. Consuming the body without checking that the status is okay It seems like a latent bug to me.
Because a body without an HTTP status code can't be trusted. |
Totally agree and that's why I mentioned that the method should check the status code and fail if it's not a success. |
Ouch, I should finish reading before replying 😊 |
Guess what someone just asked in gitter? https://gitter.im/crystal-lang/crystal?at=5fb01edab4283c208a656378 I can't believe there's so much resistance to make this trivial change. |
And I say it's trivial because I already have this implemented in my machine, I'm just waiting for an "okay" so I can send a PR. |
I don't see any resistance to improving this. We just need to agree on the solution. IMO having to separate types is completely unnecessary. We really need only a single type with IO. I think the best improvement we can do right now is to add Just adding a fallback for |
If you take a look at newer languages like Rust, Elm... even Haskell has this... you define all possible states a type can be (algebraic data type) and then there's no way to represent an invalid state. In my proposal, when you fetch a non streaming response, you can only read it as a string. If you are streaming it, you can only treat it as an IO. Of course you can explicitly go from one to the other (build an IO memory or read everything into a string) but as it's been avoided all the time here, implicit behavior is not good. I sincerely can't see the downside of having two types other than hypothetical situations. |
A very practical downside from the API design is that IMO a non-streaming type is simply not necessary at all. So we can eventually have just a single type with streaming IO. No type with buffered string body. |
How can you make it so that users don't forget to close the IO? If you have a solution for that, I can concede. |
I'd suggest to remove the overload that returns a That would also free up the non-yielding overload for a convenience method that directly returns |
I think we are going in circles here so I'll just stop. Sorry :-) |
You're probably right. I'll still add this to my last response because I might have been thinking to far ahead: We can obviously also keep the non-yielding overload and have it return a |
Which is what I suggested in #9901 (comment) and #9901 (comment) already :P |
Yes and I in #9901 (comment) and @bcardiff in #9901 (comment). It essentially just differs on whether to stay with |
Oh, I really like that. @jhass I think I missed that third option, sorry. So, to see if I understand things correctly, the idea is:
The nice thing about this approach is that you always deal with a response in the same way: it's an IO. The not-so-nice thing about this is that for the non-streaming version you have to do
So I think I'm 👍 for this approach. It's a breaking change, but it's probably better to do it before 1.0 is released. |
Yes, that's the idea.
|
Great 👍 Sounds like we're pretty much agreed then. It seems
Staying with I'm in favor of |
I would strongly prefer |
Context
I think it's a shame that having a compiled and type-safe language we are using the same type for non-streaming (consumed) and streaming responses:
The "okay" and "not okay" here mean that when it's "not okay" we get a runtime error. This is just a minor thing because you'll probably notice it right away and change it to use the other. However, it can be quite confusing to understand why you get that error. And also, we could catch this at compile-time.
Proposal
I propose we have this hierarchy:
String
body.IO
bodySome decisions to make
body
property in both cases? That means you can always usebody
(nice) but the type changes depending on whether it's in a block or not. The downsides are that it can be a bit confusing, and it's also a breaking change. Alternatively we can keep it asbody
andbody_io
so it's not a breaking change.The text was updated successfully, but these errors were encountered: