Steve
April 22, 2025, 4:21pm
1
http/server… How to get the request body?
The POST body from the client is JSON in the format below:
{
"service": "serviceName",
"method": "methodName",
"params": {}
}
Server side I tried to retrieve that like this:
postbody = context.request.body.to_s
but that gives me something like: #<HTTP::FixedLengthContent:0x7f30dcf19850>
How do I get that request body as a string?
HTTP::FixedLengthContent
is an IO
stream. You can consume it partially (bytewise), or entirely with #gets_to_end
:
postbody = context.request.body.gets_to_end
Steve
April 22, 2025, 5:09pm
3
I can’t even run with that, I get the following error:
Error: undefined method 'gets_to_end' for Nil (compile-time type is (IO | Nil))
It’s possible a request does not have a body, so it could be nil
. If you are 100% sure there will be one, could do something like context.request.body.not_nil!.gets_to_end
. Otherwise might be better to check for one and return a proper HTTP error back.
Steve
April 22, 2025, 5:51pm
5
Thank you.
If the body is missing can I be sure this will just resolve to an empty string?
No, if the body is missing and you’re using .not_nil!
it would likely result in an exception which would likely return a 500
error to the user. If this code is going to be used for real, I’d consider explicitly handling it by returning a 400
or something if there is no body and you’re expecting one.
Steve
April 22, 2025, 6:30pm
7
Sorry, I’m new to Crystal. I love the syntax and I seemed to be doing OK until I hit this bump in the road
How can I test for nil in my code when the compiler won’t even let me run when that possibility exists?
Error: undefined method 'gets_to_end' for Nil (compile-time type is (IO | Nil))
Something like this should do it, assuming you’re using the default HTTP server and not a framework:
unless body = context.request.body
break context.response.respond_with_status(400, "Request is missing a body")
end
puts body.gets_to_end
Alternative:
context.request.body.try(&.gets_to_end)
This evaluates to either a string containing the body content, or nil
if there’s no body.
Steve
April 23, 2025, 8:10am
10
Thank you both for your help.
This had me going for a while but I’ve found even if there is no body in the POST request context.request.body
is still not nil…
unless body = context.request.body
puts "It's nil"
return
end
puts typeof(body) # IO
postbody = body.gets_to_end
puts typeof(postbody) # String
if postbody == ""
puts "It's an empty string!" # is displayed
end
What does the POST
request look like?
Steve
April 23, 2025, 9:10am
12
I just commented out the body:
const response = await fetch("/", {
method: "POST",
//body: JSON.stringify({"service": "foo", "method": "bar", "params": ["baz"]})
});
console.log(response);
And now I currently have this in the server’s handler:
begin
js = JSON.parse(context.request.body.not_nil!.gets_to_end)
rescue
do_error(context, HTTP::Status::BAD_REQUEST, "Malformed request", "text/plain")
return
end
It’s possible the body ain’t nil but empty? Like the server is returning content-length zero or a chunked response with an empty body?
BTW: the nilable body sounds a bit harsh. Couldn’t it always return an empty IO object?
1 Like
That JS request sends the header Content-Length: 0
. That implies an empty body (not a nil body), just as you observed.
I was thinking the same.
It’s quite rare that the body is actually nil.
Oh, yes, please!
I had so many fights with code readability due that little detail.
Ended always with this noop pattern:
# POST /
root do
post do
begin
content = parse_json(context.request.body, JokeInput).try(&.[:content]?)
if content
if joke = repo.create_joke(content)
status 201
json({
joke: joke,
})
else
error 400, "Could not create joke"
end
else
error 400, "Content is required and cannot be empty"
end
rescue ex : JSON::ParseException
error 400, "Invalid JSON payload"
This file has been truncated. show original
Steve
April 23, 2025, 11:25am
16
Quite a neat bit of lateral thinking there imo
3 Likes