-
-
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
Set HTTP::Headers in read-only mode after HTTP::Headers were written #8877
Conversation
src/http/headers.cr
Outdated
def read_only? | ||
@read_only | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def read_only? | |
@read_only | |
end | |
getter? read_only : Bool |
class ReadOnlyError < Exception | ||
def initialize(message) | ||
super(message) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why defining an initialize
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the message is optional in Exception
. I want to force users of this exception to provide why the type was set to read-only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@asterite If so, then it needs to be defined with String
argument restriction, otherwise you could pass a nil
as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error is supposed to be re-used elsewhere? It may be a better idea to have a dedicated error for reading a read-only header.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having both will be the best, to rescue only headers errors, or only read-only errors - or both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure here, why using a global ReadOnly
error then?
I don't think all ReadOnly
exceptions (if any) would be on this case.
Just create a dedicated error where it is used; in the HTTP::Headers
module - more importantly if not planned to be reused elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the standard library shouldn't be raising Exception
, it should be raising specific errors. Slice was already doing that and I thought about using the same exception type because the reason is the same: an object was set to read-only mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, just wondering about where to place this very error, to provide more context, which will prevent to specify HTTP::Headers
in the message (and just say headers
)...
That's not a big deal then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking of something like Cannot modify headers (HTTP::Headers::ReadOnlyError)
instead of HTTP::Headers are in read-only (ReadOnlyError)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just my opinion, but I don't think discussing these things matter.
I think the top-level error is fine, and eventually a freezable module could become handy. Do you recall why you found freeze confusing? Although since we are not implementing it in Object, I am good with going on read_only route. |
Maybe |
Not requesting a change, just want to point out that |
@vlazar I would reserve freeze only for a more general way to freeze objects, enforced by the compiler/runtime implicitly. I am not saying that that would/could happen. But since that is the usage in other languages, I would prevent the overlap and probably generate some confusion. |
To be honest, the original issue to me sounds like something the library should have solved by providing an immutable view, and not expose
We would still need some way for HTTP clients to supplement header "ergonomics", for example, supplying the correct In which case, I'd probably suggest a "lower level" Then, #8712 should be a compile-time error for frameworks that directly pass the user This has other analogs in our std, for concepts that construct
Downsides would be that the deprecation period for the mutable methods would affect a lot of code, I imagine, so I don't think the above is compelling enough. But, WDYT? |
@z64 The problem with a builder approach is that HTTP headers are often not constructed at a single place, but dispersed over several handlers, middleware, framework code etc. I don't see how a builder could improve that without removing the ability to set header values somewhere in the HTTP handling stack (as long as it's before sending the first byte). |
Right. Conceptually The intention was to divide
|
I understand that that would in theory bring a bit more type-safety. In practice, as soon as you make this mistake you'll find out while testing the app. I believe doing it the functional approach is more suitable to functional languages, not Crystal. It would make the code a lot harder to understand and follow, for a very small benefit. |
@z64 Yes, |
src/http/headers.cr
Outdated
@@ -356,4 +377,10 @@ struct HTTP::Headers | |||
end | |||
end | |||
end | |||
|
|||
private def check_writeable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's writable
not writeable
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ready for another review |
Ready for yet another review. I had to add a way to unmark it as sent, because the request processor reuses http headers. So, I made it a property. |
headers["Foo"].should eq("Bar") | ||
end | ||
|
||
it "can still read them" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This spec seems duplicate.
@@ -50,6 +50,7 @@ class HTTP::Server | |||
def reset | |||
# This method is called by RequestProcessor to avoid allocating a new instance for each iteration. | |||
@headers.clear | |||
@headers.sent = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this happen in Headers#clear
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was my first thought, but I'm not sure. It's not obvious that any time you want to clear the headers you also want to mark them as not sent.
I think that clear
method delegates to Hash, so effectively jumping over the sent?
guard. I think HTTP::Headers
should stop forwarding stuff to Hash. Then we need to move sent = false
above this line.
This looks like it will take a long time to settle, so for now I'm abandoning this PR. Anyone feel free to continue this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clear
already changes the headers, so it can't work any other way than with sent == false
. It either needs to call check_not_sent
or implicitly set sent = false
. When doing the latter, we can also revert from sent=
to mark_as_send!
and the only way to remove the sent flag is calling clear
. That should actually cover all use cases.
Maybe clear
shoud be renamed to reset
to better communicate its purpose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing is, imagine the headers were already sent. Someone calls clear
, appends a few headers, everything works fine. Except that it has no effect on the output because the headers were sent and you didn't get a runtime error.
This is what I'm trying to avoid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then clear
shouldn't be part of the public API or make it clear!
/reset!
to emphasize the possible dangers related to it.
Closing. Let's discuss what API we want in the issue and then (someone) send a single PR without discussions. |
Fixes #8712