Http2 shard

I’ve found the http2 shard, but I’m having some trouble to get things working.

Has anyone some example how to do some http/2 post request?

I assume I have the HTTP2::Client properly initialised (but I am already not entirely sure about that):

context= OpenSSL::SSL::Context::Server.new
context.private_key= "/path/to/cert.key"
context.certificate_chain= "/path/to/cert.cer"
client= HTTP2::Client.new "example.com", 443, ssl_context: context

I have prepared some headers and some body I would like to post, but I wouldn’t know how.

I have tried to modify Client#request to add a parameter for the body, which I then tried to use as in

#…
stream.send_headers headers

# the next line was added
stream.send_data body

@requests[stream].receive
#…

and tried to call it like

client.request(headers, body) do |resh, resb|
  p [resh, resb]
end

I don’t think I would have to modify the procedure, so I’m wondering what the correct way would be?
And this doesn’t even seem to matter yet as it won’t even reach that part yet anyway, because I constantly run into Unhandled exception in spawn: End of file reached (IO::EOFError) whenever read_frame_header gets called.

Maybe someone has some simple example of sending some data as a http2 post request!?

If it’s not clear what I even try to achieve, this is how my code would look like in ruby (using the net-http2 gem)

client= NetHttp2::Client.new "https://example.com:443", ssl_context: OpenSSL::SSL::SSLContext.new.add_certificate(OpenSSL::X509::Certificate.new(File.read "/path/to/cert.cer"), OpenSSL::PKey.read(File.read "/path/to/cert.key"))
headers= {…}
body= {…}
path= "/foo/bar"
response= client.call :post, path, body: body, headers: headers
#=> #<NetHttp2::Response:0x00007f9deba42e60 @body="…", @headers={":status"=>"200", …}>

Thanks in advance!

1 Like

@kjarex One of the slightly awkward things about Crystal shards is that there isn’t necessarily a single shard with a given name, so a link might offer more context. Do you mean this http2 shard?

1 Like

Oh, good point. Yes, that’s the one.

I’ve run into a few different issues with that shard, unfortunately. I don’t think it was ever finalized to be loaded as a shard because there’s still some experimental code lying around — for example, require "http2/client" executes this code at the bottom of that file.

A couple years ago I wrote a grpc shard where I implemented H2 from scratch. Julien’s shard at the time didn’t implement all the features of H2 required for gRPC (notably, it assumed headers only came before the body, as in HTTP/1.1) and I didn’t understand it enough to contribute a patch, so I learned by writing it from scratch and tested by having it talk back and forth with the Ruby grpc gem. :upside_down_face:

It works-ish, both as client and server, but it doesn’t implement CONTINUATION frames so if request or response bodies are larger than a single DATA frame it just fails. But also I didn’t test it with anything other than simple gRPC so if you’re looking for a general-purpose H2 library this may not be sufficient. If it is, feel free to steal with attribution. I haven’t used gRPC or anything else on top of H2 since I left the company I built that for.

1 Like

Have you tried/looked at Duo instead?

Hope that helps.

3 Likes

This is amaaaazing. :star_struck: I don’t know how I haven’t seen this before.

Already a big “Thank you” to the both of you - I’ll have a look!

1 Like

Hi I created the Duo shard,

Im not an expert so the shard might have some quirks, please provide any feedback on the Duo shard. I’m single developer and any contribution Will be greatly appreciated by the Crystal Community

3 Likes

Thanks everyone.

I’ve got the http2 shard working for my purposes, with just some minor changes/ additions (a tiny bit of it I believe to be necessary, and the rest I added just for my convenience).

If someone else runs into the same issue, the following stuff might help you:

Add this to the shard’s client.cr:

module HTTP2  
  class Client    
    class Response
      getter headers : HTTP::Headers, status : Int32, body : String
      def initialize(stream : Stream)
        @status= stream.headers[":status"]? ? stream.headers[":status"].to_i : 0
        @body= stream.data.size==0 ? "" : stream.data.gets_to_end
        @headers= stream.headers
      end
    end
    
    def self.new(uri : String, cert : String, key : String)
      uri_= URI.parse uri
      if uri_.scheme=="https"
        port= uri_.port || 443
        ctx= OpenSSL::SSL::Context::Client.from_hash({"key" => key, "cert" => cert, "verify_mode" => "none"})
      else
        port= uri_.port || 80
        ctx= false
      end      
      self.new uri_.host.as(String), port, ssl_context: ctx
    end
    
    def request(method, path, body, headers : Hash(String, String)?=nil) : Response
      hh= HTTP::Headers{":method" => method, ":path" => path, ":authority" => @authority, ":scheme" => @scheme}
      hh["content-length"]= body.bytesize.to_s if method=="POST"
      hh.merge!(headers) if headers
      stream= @connection.streams.create
      @requests[stream]= Channel(Nil).new

      stream.send_headers hh      
      stream.send_data body, Frame::Flags::END_STREAM if method=="POST"
      @requests[stream].receive
      # I actually have no idea if the next line is necessary or not - feel free to find this out for yourself :)
      # for my specific use case at least, it doesn't seem to be required
      stream.send_rst_stream(Error::Code::NO_ERROR) if stream.active?
      Response.new stream
    end
    
    def get(path, headers : Hash(String, String)?=nil) : Response
      request "GET", path, "", headers
    end
    
    def post(path, body, headers : Hash(String, String)?=nil) : Response
      request "POST", path, body, headers
    end
  end
end

And then you can use it like this:

client= HTTP2::Client.new "https://example.com", "/path/to/cert.cer", "/path/to/cert.key"

p client.request "POST", "/some/path", payload, additional_headers
# => #<HTTP2::Client::Response:0x108a89c90 @headers=HTTP::Headers{":status" => "200", …}, @status=200, @body="…">

# alternatively you could have written the last line also like this:
p client.post "/some/path", payload, additional_headers
# => #<HTTP2::Client::Response:0x108a89c90 @headers=HTTP::Headers{":status" => "200", …}, @status=200, @body="…">

client.close

My code can be for sure improved, and I wouldn’t be surprised if it contains even some major bugs! Don’t just copy/paste it, if you are going to use it for anything serious (or do it, but be aware of the potential risks). It’s just meant to help you getting started if you are similar lost like I was a few days ago.
I’m using it currently exactly like this (to send push notifications to Apple devices) and it seems to work fine for me, but I might be just lucky.

1 Like

Maybe fork the original and submit a PR for the fix? :slight_smile:

1 Like

I would do so, but sadly the code I posted won’t work universally. I have no understanding of http2 and was only able to adjust it to work for my needs (by modifying it until the server eventually accepted my requests and sent the response I expected to receive).

Of my very little understanding of http2 I am absolutely certain that most other use cases will require further adjustment (in particular whenever the payload would need to be split up into multiple frames, I think). In a best case scenario my code might be useful to someone as a starting point to do further adjustments, but it’s in no way something that should be expected to just work as it is (because in most cases it won’t). And while it works for my use case, I am not even certain if it might not contain some mayor bugs there.