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

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

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)

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)

class App
  include HTTP::Handler

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

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"),

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