Why not_nil! must be applied on request.body?

I have just learnt that to get the content of the body of POST in HTTP::Handler you have to use the method not_nil! in this way: context.request.body.not_nil!.gets_to_end. I tried to read documentation but I don’t understand.

Technically you don’t need to use not_nil! for this, but it’s a valid choice if you can be 100% sure the request will always have a body. Otherwise you should treat it as any nilable value and properly handle the case that it is nil, such as someone does a POST request but without a body.

unless body = context.request.body
  # Handle case where there is no request body
end

body # : IO - Nil type is removed

But i tried to read the body content as a string and I got an object, how can I extract the string from that object?

You would want to use IO - Crystal 1.11.2 on the request body IO object. Depending on your use case, you may be able to even use the IO object directly.

If you really just want the string, you can call body.gets_to_end (once you’ve convinced the compiler that body can’t be nil):

if body = request.body
  string = body.gets_to_end
end

I wouldn’t do this in a real-world app (there’s nothing stopping a client from sending a 10GB HTTP request, so you probably don’t want to keep it all in RAM), but it’s perfectly fine for figuring things out, especially if working with strings helps you learn.

Like @Blacksmoke16 mentioned, Crystal has a lot of ways to process data directly off the wire specifically so you don’t have to load it all into RAM first. Like, if you want to parse it into JSON, you can call YourModel.from_json(body) where the body has yet to be received by the server. It will be parsed directly from the wire into your Crystal object without holding an intermediate string representation in memory. Here’s an example (showing the HTTP client and server in separate fibers for brevity):

require "http"
require "json"
require "uuid/json"
require "uri/json"

struct Post
  include JSON::Serializable

  getter id : UUID
  getter content : String
  getter author : User
  getter timestamp : Time

  def initialize(*, @id = UUID.random, @content, @author, @timestamp = Time.utc)
  end
end

struct User
  include JSON::Serializable

  getter id : UUID
  getter username : String
  getter display_name : String
  getter avatar_url : URI

  def initialize(*, @id = UUID.random, @username, @display_name, @avatar_url)
  end
end

class App
  include HTTP::Handler

  def call(context)
    if body = context.request.body
      post = Post.from_json(body)
      pp deserialized_post: post
    else
      context.response.status = :bad_request
    end
  end
end

spawn HTTP::Client.post(
  url: URI.parse("http://localhost:8080/"),
  body: Post.new(
    content: "Hello world",
    author: User.new(
      username: "jgaskins",
      display_name: "Jamie",
      avatar_url: URI.parse("https://sjc6.discourse-cdn.com/standard10/user_avatar/forum.crystal-lang.org/jgaskins/96/92_2.png"),
    ),
  ).to_json
)

http = HTTP::Server.new(App.new)
http.listen 8080
2 Likes