Giter Site home page Giter Site logo

mtrudel / bandit Goto Github PK

View Code? Open in Web Editor NEW
1.5K 26.0 73.0 9.04 MB

Bandit is a pure Elixir HTTP server for Plug & WebSock applications

License: MIT License

Elixir 100.00%
elixir elixir-phoenix http https http2 http-server http2-server h2spec elixir-plug rfc-7540

bandit's Introduction

Bandit Bandit

Build Status Docs Hex.pm

Bandit is an HTTP server for Plug and WebSock apps.

Bandit is written entirely in Elixir and is built atop Thousand Island. It can serve HTTP/1.x, HTTP/2 and WebSocket clients over both HTTP and HTTPS. It is written with correctness, clarity & performance as fundamental goals.

In ongoing automated performance tests, Bandit's HTTP/1.x engine is up to 4x faster than Cowboy depending on the number of concurrent requests. When comparing HTTP/2 performance, Bandit is up to 1.5x faster than Cowboy. This is possible because Bandit has been built from the ground up for use with Plug applications; this focus pays dividends in both performance and also in the approachability of the code base.

Bandit also emphasizes correctness. Its HTTP/2 implementation scores 100% on the h2spec suite in strict mode, and its WebSocket implementation scores 100% on the Autobahn test suite, both of which run as part of Bandit's comprehensive CI suite. Extensive unit test, credo, dialyzer, and performance regression test coverage round out a test suite that ensures that Bandit is and will remain a platform you can count on.

Lastly, Bandit exists to demystify the lower layers of infrastructure code. In a world where The New Thing is nearly always adding abstraction on top of abstraction, it's important to have foundational work that is approachable & understandable by users above it in the stack.

Project Goals

  • Implement comprehensive support for HTTP/1.0 through HTTP/2 & WebSockets (and beyond) backed by obsessive RFC literacy and automated conformance testing
  • Aim for minimal internal policy and HTTP-level configuration. Delegate to Plug & WebSock as much as possible, and only interpret requests to the extent necessary to safely manage a connection & fulfill the requirements of safely supporting protocol correctness
  • Prioritize (in order): correctness, clarity, performance. Seek to remove the mystery of infrastructure code by being approachable and easy to understand
  • Along with our companion library Thousand Island, become the go-to HTTP & low-level networking stack of choice for the Elixir community by being reliable, efficient, and approachable

Project Status

  • Complete support for Phoenix applications (WebSocket support requires Phoenix 1.7+)
  • Complete support of the Plug API
  • Complete support of the WebSock API
  • Complete server support for HTTP/1.x as defined in RFC 9112 & RFC 9110
  • Complete server support for HTTP/2 as defined in RFC 9113 & RFC 9110, comprehensively covered by automated h2spec conformance testing
  • Support for HTTP content encoding compression on both HTTP/1.x and HTTP/2. gzip and deflate methods are supported per RFC9110§8.4.1.{2,3}
  • Complete server support for WebSockets as defined in RFC 6455, comprehensively covered by automated Autobahn conformance testing. Per-message compression as defined in RFC 7692 is also supported
  • Extremely scalable and performant client handling at a rate up to 4x that of Cowboy for the same workload with as-good-or-better memory use

Any Phoenix or Plug app should work with Bandit as a drop-in replacement for Cowboy; exceptions to this are errors (if you find one, please file an issue!).

Using Bandit With Phoenix

Bandit fully supports Phoenix. Phoenix applications which use WebSockets for features such as Channels or LiveView require Phoenix 1.7 or later.

Using Bandit to host your Phoenix application couldn't be simpler:

  1. Add Bandit as a dependency in your Phoenix application's mix.exs:

    {:bandit, "~> 1.0"}
  2. Add the following adapter: line to your endpoint configuration in config/config.exs, as in the following example:

    # config/config.exs
    
    config :your_app, YourAppWeb.Endpoint,
      adapter: Bandit.PhoenixAdapter, # <---- ADD THIS LINE
      url: [host: "localhost"],
      render_errors: ...
  3. That's it! You should now see messages at startup indicating that Phoenix is using Bandit to serve your endpoint, and everything should 'just work'. Note that if you have set any exotic configuration options within your endpoint, you may need to update that configuration to work with Bandit; see the Bandit.PhoenixAdapter documentation for more information.

Using Bandit With Plug Applications

Using Bandit to host your own Plug is very straightforward. Assuming you have a Plug module implemented already, you can host it within Bandit by adding something similar to the following to your application's Application.start/2 function:

# lib/my_app/application.ex

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      {Bandit, plug: MyApp.MyPlug}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

For less formal usage, you can also start Bandit using the same configuration options via the Bandit.start_link/1 function:

# Start an http server on the default port 4000, serving MyApp.MyPlug
Bandit.start_link(plug: MyPlug)

Configuration

A number of options are defined when starting a server. The complete list is defined by the t:Bandit.options/0 type.

Setting up an HTTPS Server

By far the most common stumbling block encountered when setting up an HTTPS server involves configuring key and certificate data. Bandit is comparatively easy to set up in this regard, with a working example looking similar to the following:

# lib/my_app/application.ex

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      {Bandit,
       plug: MyApp.MyPlug,
       scheme: :https,
       certfile: "/absolute/path/to/cert.pem",
       keyfile: "/absolute/path/to/key.pem"}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

WebSocket Support

If you're using Bandit to run a Phoenix application as suggested above, there is nothing more for you to do; WebSocket support will 'just work'.

If you wish to interact with WebSockets at a more fundamental level, the WebSock and WebSockAdapter libraries provides a generic abstraction for WebSockets (very similar to how Plug is a generic abstraction on top of HTTP). Bandit fully supports all aspects of these libraries.

Implementation Details

Bandit primarily consists of three protocol-specific implementations, one each for HTTP/1, HTTP/2 and WebSockets. Each of these implementations is largely distinct from one another, and is described in its own README linked above.

If you're just taking a casual look at Bandit or trying to understand how an HTTP server works, the HTTP/1 implementation is likely the best place to start exploring.

Contributing

Contributions to Bandit are very much welcome! Before undertaking any substantial work, please open an issue on the project to discuss ideas and planned approaches so we can ensure we keep progress moving in the same direction.

All contributors must agree and adhere to the project's Code of Conduct.

Security disclosures should be handled per Bandit's published security policy.

Installation

Bandit is available in Hex. The package can be installed by adding bandit to your list of dependencies in mix.exs:

def deps do
  [
    {:bandit, "~> 1.0"}
  ]
end

Documentation can be found at https://hexdocs.pm/bandit.

License

MIT

bandit's People

Contributors

aaronrenner avatar aerosol avatar alisinabh avatar asakura avatar crertel avatar cvkmohan avatar danschultzer avatar dependabot[bot] avatar derekkraan avatar dethi avatar dmorneau avatar dorian-marchal avatar gregors avatar jbraungardt avatar jclem avatar jjcarstens avatar jonatanklosko avatar moogle19 avatar mtrudel avatar mwhitworth avatar nbw avatar nelsonmestevao avatar patrickjaberg avatar ryanwinchester avatar sabiwara avatar solar05 avatar travisgriggs avatar v0idpwn avatar wojtekmach avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

bandit's Issues

Advertise HTTP/1.1 via ALPN

We already advertise h2 (as required by RFC7540), but should also do HTTP/1.1. Configuration of this isn't required for 0.4.x; we'll be adding in broader protocol configuration in 0.6.x

shutdown_timeout is not accepted

When passing shutdown_timeout as an option Bandit refuses to start with the following error message:

Unsupported keys(s) in options config: [:shutdown_timeout]

After a quick search through the code the only reference to :shutdown_timeout is in the docs. It doesn't seem to be used for anything in practice.

Is this still not implemented?

Compress response body

Within our Phoenix application we set the compress option in the endpoint http config, Cowboy then tries to compress the response body. Looking through the Bandit documentation this doesn't seem possible. Are there any plans to add this or is this maybe already possible?

config :web, Web.Endpoint,
  http: [compress: true]

Host port should default to 80 if the Host header has been set

Assuming we want to keep compatibility with cowboy:

When the host header is set with cowboy without an explicit port, cowboy will default to port 80.

To reproduce:

curl -H "Host: foo.co.za" http://localhost:4000

Should set conn.port to 80

curl http://localhost:4000

Should set conn.port to 4000

and

curl -H "Host: foo.co.za:9999" http://localhost:4000

Should set conn.port to 9999

This is currently a issue with ueberauth when generating the callback_url.

no function clause matching in Bandit.HTTP1.Handler.handle_info/2

I'm seeing this quite a lot in my logs.
Is there anything I am doing wrong?

[2023-02-05 05:50:00.373] request_id=F0DUZvMXywDyPHsAAEBi [error] GenServer #PID<0.1940.0> terminating
** (FunctionClauseError) no function clause matching in Bandit.HTTP1.Handler.handle_info/2            
    (bandit 0.6.8) lib/thousand_island/handler.ex:5: Bandit.HTTP1.Handler.handle_info({:change_modal_mode}, {%ThousandIsland.Socket{socket: #Port<0.42>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 15000}, %{handler_module: Bandit.HTTP1.Handler, plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {BackendWeb.Endpoint, []}}}})
    (stdlib 4.0.1) gen_server.erl:1120: :gen_server.try_dispatch/4                                    
    (stdlib 4.0.1) gen_server.erl:1197: :gen_server.handle_msg/6                                      
    (stdlib 4.0.1) proc_lib.erl:240: :proc_lib.init_p_do_apply/3                                      
Last message: {:change_modal_mode}

Improve use of iodata

There are a number of places (particularly in the h2 stack) where we're building up buffers based on iodata input and end up needing to flatten them to binaries earlier than is optimal. Mostly this comes down to the fact that there is no way to grab the first n bytes of an iodata without resorting to pattern matching:

# Given data like...
data = ["a", ["b", [[], "c",......,"z"]

# Instead of this...
<first_three_bytes::binary-size(3), rest::binary> = IO.iodata_to_binary(data)

# It would be good to do this (or something similar)....
{first_three_bytes, rest} = IO.split_iodata(data, 3)

Some provisional work for this is at https://github.com/mtrudel/iolist_test, but it's something around 8x slower than binary approaches. I'm assuming that this is mostly because binary conversion / matching is done in C within the BEAM, while my naive iolist splitting heuristic is being done in Elixir. To my eye there's strictly less work to be done in the iolist splitting scenario, so if it were implemented in C I imagine it would be faster / more memory efficient than the binary conversion / matching approach.

It would be useful to understand if / to what extent this is the case, and whether improved iolist primitives in the BEAM would be worth it.

dependency issue with 1.0.0-pre.1

amazing library, looking to incorporate this and try it out in our prod phx application.

running into the following error

#Incompatibility<#Term<your app>, cause: {:conflict, #Incompatibility<#Term<phoenix >= 1.7.0-rc.0>, cause: {:conflict, #Incompatibility<#Term<phoenix >= 1.7.0-rc.0>, #Term<not bandit ~> 0.5.9 or ~> 0.6 (optional)>, cause: {:conflict, #Incompatibility<#Term<phoenix >= 1.7.0-rc.0>, #Term<not websock_adapter ~> 0.4>, cause: :dependency>, #Incompatibility<#Term<not bandit ~> 0.5.9 or ~> 0.6 (optional)>, #Term<websock_adapter>, cause: {:conflict, #Incompatibility<#Term<websock_adapter >= 0.4.5>, #Term<not bandit ~> 0.6 (optional)>, cause: :dependency>, #Incompatibility<#Term<websock_adapter < 0.4.5>, #Term<not bandit ~> 0.5.9 (optional)>, cause: :dependency>}>}>, #Incompatibility<#Term<your app>, #Term<not bandit ~> 1.0.0-pre.1>, cause: :dependency>}>, #Incompatibility<#Term<your app>, #Term<not phoenix ~> 1.7.2>, cause: :dependency>}>
Resolution completed in 0.884s
Because websock_adapter >= 0.4.5 depends on bandit ~> 0.6 and websock_adapter < 0.4.5 depends on bandit ~> 0.5.9, websock_adapter requires bandit ~> 0.5.9 or ~> 0.6.
And because phoenix >= 1.7.0-rc.0 depends on websock_adapter ~> 0.4, phoenix >= 1.7.0-rc.0 requires bandit ~> 0.5.9 or ~> 0.6.
And because your app depends on bandit ~> 1.0.0-pre.1, phoenix >= 1.7.0-rc.0 is forbidden.
So, because your app depends on phoenix ~> 1.7.2, version solving failed.

we did delete the mix.lock file just to try to resolve the deps, looks like its a core issue with websock_adaptor, any guidance is greatly appreciated!

Question: Differences between Cowboy and Bandit

Hello. No issue. Just a general question.

What are the differences between Cowboy and Bandit? I understand that Bandit is 100% Elixir.

But if I'm proposing this to my Lead, I'm wanting to show him just what the differences are, all the way down to the most basic level.

Thanks for any info you may have.

`Plug.Conn.send_resp` in separate process fails for HTTP/2 streams

Took me a while to track this one down. When testing with HTTP/2 I was seeing failure with Stream 3 completed in unexpected state remote_closed and empty responses even though I definitely did send the response!

The culprit is this line (the same for send_data):

:ok <- Stream.owner?(stream, pid),

In TestServer the route matching lives in a GenServer. When a requests hits the generic plug on the HTTP server, the plug will pass off the conn to the genserver to be processed in a handle_call callback. This means that caller pid will be different when calling send_resp.

Resolving this in TestServer may be awkward since the routing state lives in that GenServer process. Maybe it does make sense that the send_resp call should only happen in the generic plug process. But what's the reasoning behind the owner?/2 check? Why is it necessary to lock the conn processing to the initial caller?

FWIW cowboy works with the current logic in TestServer. It works as expected with Bandit when I remove this check, but not sure if there are any sideeffects to doing that. If it's necessary to have this check then it would be good to have a clearer warning.

Error when sending messages to liveview processes

Hi! I heard about bandit on the ThinkingElixir podcast and wanted to give it a try. I found what I think might be a bug.

I am using bandit 0.6.7, thousand_island 0.5.15, phoenix 1.7.0-rc.2, elixir 1.14.0 and erlang 25.0.

I make a new phoenix project via mix archive.install hex phx_new 1.7.0-rc.2 ; mix phx.new foo - then I replace plug_cowboy with {:bandit, "~> 0.6.7"} in hex.exs and add adapter: Bandit.PhoenixAdapter in config/config.exs.

If I send any messages to a liveview process, the first time such a message is sent, I see the error (FunctionClauseError) no function clause matching in Bandit.HTTP1.Handler.handle_info/2 (more lines attached below). Subsequent messages do not repeat the same error. The liveview's handle_info callback is invoked in both cases.

Example liveview code

def mount(_params, _session, socket) do
  :timer.send_interval(1000, :tick)
  {:ok, assign(socket}
end

def handle_info(:tick, socket) do
  IO.inspect("tick happened")
  {:noreply, socket}
end

Full error output:

[error] GenServer #PID<0.2882.0> terminating                                                                                   
** (FunctionClauseError) no function clause matching in Bandit.HTTP1.Handler.handle_info/2
    (bandit 0.6.7) lib/thousand_island/handler.ex:5: Bandit.HTTP1.Handler.handle_info(:tick, {%ThousandIsland.Socket{socket: #Port<0.55>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 15000}, %{handler_module: Bandit.HTTP1.Handler, plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {FartWeb.Endpoint, []}}}})                                                              
    (stdlib 4.0) gen_server.erl:1120: :gen_server.try_dispatch/4                                                               
    (stdlib 4.0) gen_server.erl:1197: :gen_server.handle_msg/6                                                                 
    (stdlib 4.0) proc_lib.erl:240: :proc_lib.init_p_do_apply/3                                                                 
Last message: :tick                                            
State: {%ThousandIsland.Socket{socket: #Port<0.55>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 15000}, %{handler_module: Bandit.HTTP1.Handler, plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {FartWeb.Endpoint, []}}}}
tick happened
tick happened
tick happened
...

Add support for HTTP/2 originated Websockets (RFC 8441)

I was talking to @mtrudel on slack and he came up with a list of steps in order to support http/2 h2c and http2 websockets:

  1. Add support for upgrade support to bandit/http1/{adapter,handler}.ex. This can mostly be cribbed directly from websocket support. I wouldn’t worry about parsing the request details here; just assume that a Plug will parse them out and pass it through as part of the opts in some format.
  2. Get it ending up as a {:switch, Bandit.HTTP2.Handler, opts} call that passes the opts through to the HTTP2 handler
  3. Add support to bandit/http2/handler.ex ’s handle_connection/2 function for starting up an HTTP2 request based on the opts passed in above. Per RFC7540§3.2 this will need to know about settings & the HTTP/1 request that included the upgrade request.
  4. Implement a Plug that knows how to read Upgrade requests, package the request & settings header up, and call the Plug.Conn.upgrade_adpater/4 that was added in step 1 above.
  5. Finally, we can shim this plug in as part of the overall plug pipeline internal to Bandit, at least for HTTP/1 requests.

May make sense to tackle step 4 first, depending on how you break the problem down in your head

Multiple/dynamic SSL certificates?

Hi, I'm considering building a pure elixir reverse proxy library and I'm looking at Bandit as a possible option to be the server at the core of it.

One of the main features would be on-demand SSL certificates for sites dynamically added through an API layer, using an acme protocol client (that I'll need to write).

Looking at the docs, SSL handling for Bandit currently seems to be locked to a single domain and cert. I'm wondering if you'd be open to the idea of allowing dynamic domains and certs that can be updated during runtime. If so, do you have any thoughts about how that should be handled and how best I could contribute to that?

Thanks!

[warning] Could not determine a protocol

I see a lot of "[warning] Could not determine a protocol" messages, often in quick succession, when accessing my Phoenix dev server over HTTP. As it stands, this warning is rather noisy. It would be great if it could be a little more informative, or else removed altogether.

Elixir 1.14.3
Phoenix 1.7.0-rc.2
Windows 10

Error when using graphql-ws protocol

I'm trying to use Bandit with an application that uses the graphql-ws protocol:
https://github.com/wodup/absinthe_graphql_ws

The protocol allows for sending connection params (e.g. a token), and the server can close the socket if e.g. the token is invalid:
https://github.com/wodup/absinthe_graphql_ws/blob/main/lib/absinthe/graphql_ws/transport.ex#L129

However I get this error when using Bandit. With a regular Phoenix/cowboy setup, this error does not occur.

** (FunctionClauseError) no function clause matching in Bandit.WebSocket.Connection.do_deflate/3
    (bandit 0.6.9) lib/bandit/websocket/connection.ex:236: Bandit.WebSocket.Connection.do_deflate({:close, 4403, "Forbidden"}, %ThousandIsland.Socket{socket: #Port<0.139>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 60000, span: %ThousandIsland.Telemetry{span_name: :connection, span_id: "XJGNI6FEZC4RZZ22", start_time: -576460747475728060}}, %Bandit.WebSocket.Connection{websock: WodUpWeb.GraphqlWSSocket, websock_state: %Absinthe.GraphqlWS.Socket{absinthe: %{opts: [context: %{pubsub: WodUpWeb.Endpoint}], pipeline: {Absinthe.GraphqlWS.Socket, :absinthe_pipeline}, schema: WodUpWeb.Graphql.Schema}, connect_info: %{}, endpoint: WodUpWeb.Endpoint, handler: WodUpWeb.GraphqlWSSocket, keepalive: 30000, pubsub: WodUp.PubSub, assigns: %{}, initialized?: false, subscriptions: %{}}, state: :open, compress: nil, fragment_frame: nil, span: %Bandit.Telemetry{span_name: :websocket, span_id: "VO6L3P3TOR4NWFO5", start_time: -576460746978892054}, metrics: %{recv_text_frame_bytes: 131, recv_text_frame_count: 1}})
    (bandit 0.6.9) lib/bandit/websocket/handler.ex:42: anonymous fn/3 in Bandit.WebSocket.Handler.handle_data/3

Here's the relevant code from the reference JS implementation of graphql-ws if that helps:
https://github.com/enisdenjo/graphql-ws/blob/799cfc7bbe0f6d1a5c90b4880f02c58c5c3a06d4/src/server.ts#L612-L614

https://github.com/enisdenjo/graphql-ws/blob/799cfc7bbe0f6d1a5c90b4880f02c58c5c3a06d4/src/__tests__/server.ts#L437-L458

file descriptor usage and emfile error

We swapped in bandit to replace cowboy, noticing these kind of error that is popping up

[error] [label: {:erl_prim_loader, :file_error}, report: 'File operation error: emfile. Target: /Users/dev/code/elixir/helio/_build/dev/lib/bandit/ebin/Elixir.String.Chars.beam. Function: get_file. Process: code_server.']

followed by these errors

ogger - error: {removed_faiLogling_handler,'Egleirx - error: ir.Logger'}
{removed_failing_handler,'Elixir.Logger'}
=ERROR REPORT==== 22-Apr-2023::16:20:07.692381 ===
{error,simple_handler_process_dead}
rogger - error: {removed_failingLog_handler,'Elixir.Loggegre r-' }e
 22-Apr-2023::16:2ling_handLogger - error: le=rE,R'REOlRi xRiErP.OLRoTg=g=e=r='}
                  0:07.692350 ===
{error,simpl{er_ehandler_process_dead}
RmEovedR_failing_handler,'Elixir.LoggeRrO'R}
 EPORT==== 22-Apr-2023::16:20:07.692558 ===
{error,simple_handler_process_dead}
=DEBUG REPORT==== 22-Apr-2023::16:20:07.692363 ===
    logger: removed_failing_handler
    handler: {'Elixir.Logger','Elixir.Logger.Handler'}
    log_event: #{level => debug,
                 meta =>
                     #{file => "logger_backend.erl",gl => <0.0.0>,
                       internal_log_event => true,line => 71,
                       mfa => {logger_backend,call_handlers,3},
                       pid => <0.42.0>,time => 1682194807530000},
                 msg =>
                     {report,
                         [{logger,remove_handler_failed},
                          {reason,
                              {attempting_syncronous_call_to_self,
                                  {remove_handler,'Elixir.Logger'}}}]}}
    config: #{config =>
                  #{counter => {atomics,#Ref<0.3609761884.478543873.105489>},
                    sasl => false,
                    thresholds => {20,500},
                    translators =>
                        [{'Elixir.Plug.Cowboy.Translator',translate},
                         {'Elixir.Logger.Translator',translate}],
                    truncate => 8096,utc_log => false},
              formatter => {logger_formatter,#{}},
              id => 'Elixir.Logger',module => 'Elixir.Logger.Handler'}
    reason: {error,undef,
                [{'Elixir.Inspect',inspect,
                     [[{logger,remove_handler_failed},
                       {reason,
                           {attempting_syncronous_call_to_self,
                               {remove_handler,'Elixir.Logger'}}}],
                      #{'__struct__' => 'Elixir.Inspect.Opts',base => decimal,
                        binaries => infer,char_lists => infer,
                        charlists => infer,custom_options => [],
                        inspect_fun => fun 'Elixir.Inspect':inspect/2,
                        limit => 50,pretty => false,printable_limit => 4096,
                        safe => true,structs => true,syntax_colors => [],
                        width => 80}],
                     []},
                 {'Elixir.Kernel',inspect,2,
                     [{file,"lib/kernel.ex"},{line,2254}]},
                 {'Elixir.Logger.Handler',do_log,4,
                     [{file,"lib/logger/handler.ex"},{line,141}]},
                 {'Elixir.Logger.Handler',log,2,
                     [{file,"lib/logger/handler.ex"},{line,84}]}]}
=ERROR REPORT==== 22-Apr-2023::16:20:07.692729 ===
{error,simple_handler_process_dead}
=DEBUG REPORT==== 22-Apr-2023::16:20:07.692339 ===
    logger: removed_failing_handler
    handler: {'Elixir.Logger','Elixir.Logger.Handler'}
    log_event: #{level => debug,
                 meta =>
                     #{file => "logger_backend.erl",gl => <0.0.0>,
                       internal_log_event => true,line => 71,
                       mfa => {logger_backend,call_handlers,3},
                       pid => <0.42.0>,time => 1682194807515457},
                 msg =>
                     {report,
                         [{logger,remove_handler_failed},
                          {reason,
                              {attempting_syncronous_call_to_self,
                                  {remove_handler,'Elixir.Logger'}}}]}}
    config: #{config =>
                  #{counter => {atomics,#Ref<0.3609761884.478543873.105489>},
                    sasl => false,
                    thresholds => {20,500},
                    translators =>
                        [{'Elixir.Plug.Cowboy.Translator',translate},
                         {'Elixir.Logger.Translator',translate}],
                    truncate => 8096,utc_log => false},
              formatter => {logger_formatter,#{}},
              id => 'Elixir.Logger',module => 'Elixir.Logger.Handler'}
    reason: {error,undef,
                [{'Elixir.Inspect',inspect,
                     [[{logger,remove_handler_failed},
                       {reason,
                           {attempting_syncronous_call_to_self,
                               {remove_handler,'Elixir.Logger'}}}],
                      #{'__struct__' => 'Elixir.Inspect.Opts',base => decimal,
                        binaries => infer,char_lists => infer,
                        charlists => infer,custom_options => [],
                        inspect_fun => fun 'Elixir.Inspect':inspect/2,
                        limit => 50,pretty => false,printable_limit => 4096,
                        safe => true,structs => true,syntax_colors => [],
                        width => 80}],
                     []},
                 {'Elixir.Kernel',inspect,2,
                     [{file,"lib/kernel.ex"},{line,2254}]},
                 {'Elixir.Logger.Handler',do_log,4,
                     [{file,"lib/logger/handler.ex"},{line,141}]},
                 {'Elixir.Logger.Handler',log,2,
                     [{file,"lib/logger/handler.ex"},{line,84}]}]}
=DEBUG REPORT==== 22-Apr-2023::16:20:07.692544 ===
    logger: removed_failing_handler
    handler: {'Elixir.Logger','Elixir.Logger.Handler'}
    log_event: #{level => debug,
                 meta =>
                     #{file => "logger_backend.erl",gl => <0.0.0>,
                       internal_log_event => true,line => 71,
                       mfa => {logger_backend,call_handlers,3},
                       pid => <0.42.0>,time => 1682194807547755},
                 msg =>
                     {report,
                         [{logger,remove_handler_failed},
                          {reason,
                              {attempting_syncronous_call_to_self,
                                  {remove_handler,'Elixir.Logger'}}}]}}
    config: #{config =>
                  #{counter => {atomics,#Ref<0.3609761884.478543873.105489>},
                    sasl => false,
                    thresholds => {20,500},
                    translators =>
                        [{'Elixir.Plug.Cowboy.Translator',translate},
                         {'Elixir.Logger.Translator',translate}],
                    truncate => 8096,utc_log => false},
              formatter => {logger_formatter,#{}},
              id => 'Elixir.Logger',module => 'Elixir.Logger.Handler'}
    reason: {error,undef,
                [{'Elixir.Inspect',inspect,
                     [[{logger,remove_handler_failed},
                       {reason,
                           {attempting_syncronous_call_to_self,
                               {remove_handler,'Elixir.Logger'}}}],
                      #{'__struct__' => 'Elixir.Inspect.Opts',base => decimal,
                        binaries => infer,char_lists => infer,
                        charlists => infer,custom_options => [],
                        inspect_fun => fun 'Elixir.Inspect':inspect/2,
                        limit => 50,pretty => false,printable_limit => 4096,
                        safe => true,structs => true,syntax_colors => [],
                        width => 80}],
                     []},
                 {'Elixir.Kernel',inspect,2,
                     [{file,"lib/kernel.ex"},{line,2254}]},
                 {'Elixir.Logger.Handler',do_log,4,
                     [{file,"lib/logger/handler.ex"},{line,141}]},
                 {'Elixir.Logger.Handler',log,2,
                     [{file,"lib/logger/handler.ex"},{line,84}]}]}
=DEBUG REPORT==== 22-Apr-2023::16:20:07.692718 ===
    logger: removed_failing_handler
    handler: {'Elixir.Logger','Elixir.Logger.Handler'}
    log_event: #{level => debug,
                 meta =>
                     #{file => "logger_backend.erl",gl => <0.0.0>,
                       internal_log_event => true,line => 71,
                       mfa => {logger_backend,call_handlers,3},
                       pid => <0.42.0>,time => 1682194807562118},
                 msg =>
                     {report,
                         [{logger,remove_handler_failed},
                          {reason,
                              {attempting_syncronous_call_to_self,
                                  {remove_handler,'Elixir.Logger'}}}]}}
    config: #{config =>
                  #{counter => {atomics,#Ref<0.3609761884.478543873.105489>},
                    sasl => false,
                    thresholds => {20,500},
                    translators =>
                        [{'Elixir.Plug.Cowboy.Translator',translate},
                         {'Elixir.Logger.Translator',translate}],
                    truncate => 8096,utc_log => false},
              formatter => {logger_formatter,#{}},
              id => 'Elixir.Logger',module => 'Elixir.Logger.Handler'}
    reason: {error,undef,
                [{'Elixir.Inspect',inspect,
                     [[{logger,remove_handler_failed},
                       {reason,
                           {attempting_syncronous_call_to_self,
                               {remove_handler,'Elixir.Logger'}}}],
                      #{'__struct__' => 'Elixir.Inspect.Opts',base => decimal,
                        binaries => infer,char_lists => infer,
                        charlists => infer,custom_options => [],
                        inspect_fun => fun 'Elixir.Inspect':inspect/2,
                        limit => 50,pretty => false,printable_limit => 4096,
                        safe => true,structs => true,syntax_colors => [],
                        width => 80}],
                     []},
                 {'Elixir.Kernel',inspect,2,
                     [{file,"lib/kernel.ex"},{line,2254}]},
                 {'Elixir.Logger.Handler',do_log,4,
                     [{file,"lib/logger/handler.ex"},{line,141}]},
                 {'Elixir.Logger.Handler',log,2,
                     [{file,"lib/logger/handler.ex"},{line,84}]}]}
[error] GenServer #PID<0.2286.0> terminating
** (UndefinedFunctionError) function String.Chars.to_string/1 is undefined or private
    (elixir 1.14.2) String.Chars.to_string(:Host)
    (bandit 0.7.7) lib/bandit/http1/adapter.ex:99: Bandit.HTTP1.Adapter.do_read_headers/5
    (bandit 0.7.7) lib/bandit/http1/adapter.ex:27: Bandit.HTTP1.Adapter.read_headers/1
    (bandit 0.7.7) lib/bandit/http1/handler.ex:22: Bandit.HTTP1.Handler.handle_data/3
    (bandit 0.7.7) lib/bandit/delegating_handler.ex:18: Bandit.DelegatingHandler.handle_data/3
    (bandit 0.7.7) lib/thousand_island/handler.ex:332: Bandit.DelegatingHandler.handle_continue/2
    (stdlib 4.1.1) gen_server.erl:1123: :gen_server.try_dispatch/4
    (stdlib 4.1.1) gen_server.erl:865: :gen_server.loop/7
    (stdlib 4.1.1) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: {:continue, :handle_connection}
State: {%ThousandIsland.Socket{socket: #Port<0.193>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 60000, span: %ThousandIsland.Telemetry{span_name: :connection, telemetry_span_context: #Reference<0.3609761884.478412809.113572>, start_time: -576459296382418083, start_metadata: %{parent_telemetry_span_context: #Reference<0.3609761884.478412805.99344>, remote_address: {127, 0, 0, 1}, remote_port: 50434, telemetry_span_context: #Reference<0.3609761884.478412809.113572>}}}, %{handler_module: Bandit.InitialHandler, opts: %{http_1: [], http_2: [], websocket: []}, plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {HelioWeb.Endpoint, []}}}}
[error] GenServer #PID<0.2282.0> terminating
** (stop) {%UndefinedFunctionError{module: String.Chars, function: :to_string, arity: 1, reason: nil, message: nil}, [{String.Chars, :to_string, [:Host], []}, {Bandit.HTTP1.Adapter, :do_read_headers, 5, [file: 'lib/bandit/http1/adapter.ex', line: 99]}, {Bandit.HTTP1.Adapter, :read_headers, 1, [file: 'lib/bandit/http1/adapter.ex', line: 27]}, {Bandit.HTTP1.Handler, :handle_data, 3, [file: 'lib/bandit/http1/handler.ex', line: 22]}, {Bandit.DelegatingHandler, :handle_data, 3, [file: 'lib/bandit/delegating_handler.ex', line: 18]}, {Bandit.DelegatingHandler, :handle_continue, 2, [file: 'lib/thousand_island/handler.ex', line: 332]}, {:gen_server, :try_dispatch, 4, [file: 'gen_server.erl', line: 1123]}, {:gen_server, :loop, 7, [file: 'gen_server.erl', line: 865]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 240]}]}
Last message: {:continue, :handle_connection}
State: {%ThousandIsland.Socket{socket: #Port<0.191>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 60000, span: %ThousandIsland.Telemetry{span_name: :connection, telemetry_span_context: #Reference<0.3609761884.478412807.136017>, start_time: -576459296420648075, start_metadata: %{parent_telemetry_span_context: #Reference<0.3609761884.478412805.99340>, remote_address: {127, 0, 0, 1}, remote_port: 50430, telemetry_span_context: #Reference<0.3609761884.478412807.136017>}}}, %{handler_module: Bandit.InitialHandler, opts: %{http_1: [], http_2: [], websocket: []}, plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {HelioWeb.Endpoint, []}}}}
[error] GenServer #PID<0.2285.0> terminating
** (UndefinedFunctionError) function String.Chars.to_string/1 is undefined or private
    (elixir 1.14.2) String.Chars.to_string(:Host)
    (bandit 0.7.7) lib/bandit/http1/adapter.ex:99: Bandit.HTTP1.Adapter.do_read_headers/5
    (bandit 0.7.7) lib/bandit/http1/adapter.ex:27: Bandit.HTTP1.Adapter.read_headers/1
    (bandit 0.7.7) lib/bandit/http1/handler.ex:22: Bandit.HTTP1.Handler.handle_data/3
    (bandit 0.7.7) lib/bandit/delegating_handler.ex:18: Bandit.DelegatingHandler.handle_data/3
    (bandit 0.7.7) lib/thousand_island/handler.ex:332: Bandit.DelegatingHandler.handle_continue/2
    (stdlib 4.1.1) gen_server.erl:1123: :gen_server.try_dispatch/4
    (stdlib 4.1.1) gen_server.erl:865: :gen_server.loop/7
    (stdlib 4.1.1) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: {:continue, :handle_connection}
State: {%ThousandIsland.Socket{socket: #Port<0.192>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 60000, span: %ThousandIsland.Telemetry{span_name: :connection, telemetry_span_context: #Reference<0.3609761884.478412807.136028>, start_time: -576459296385066446, start_metadata: %{parent_telemetry_span_context: #Reference<0.3609761884.478412805.99342>, remote_address: {127, 0, 0, 1}, remote_port: 50432, telemetry_span_context: #Reference<0.3609761884.478412807.136028>}}}, %{handler_module: Bandit.InitialHandler, opts: %{http_1: [], http_2: [], websocket: []}, plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {HelioWeb.Endpoint, []}}}}
[error] GenServer #PID<0.2050.0> terminating
** (stop) an exception was raised:
    ** (UndefinedFunctionError) function String.Chars.to_string/1 is undefined or private
        (elixir 1.14.2) String.Chars.to_string(:Host)
        (bandit 0.7.7) lib/bandit/http1/adapter.ex:99: Bandit.HTTP1.Adapter.do_read_headers/5
        (bandit 0.7.7) lib/bandit/http1/adapter.ex:27: Bandit.HTTP1.Adapter.read_headers/1
        (bandit 0.7.7) lib/bandit/http1/handler.ex:22: Bandit.HTTP1.Handler.handle_data/3
        (bandit 0.7.7) lib/bandit/delegating_handler.ex:18: Bandit.DelegatingHandler.handle_data/3
        (bandit 0.7.7) lib/thousand_island/handler.ex:343: Bandit.DelegatingHandler.handle_info/2
        (stdlib 4.1.1) gen_server.erl:1123: :gen_server.try_dispatch/4
        (stdlib 4.1.1) gen_server.erl:1200: :gen_server.handle_msg/

this started happening after the swap with bandit, looks like there is an excess usage of the file descriptor, any guidance would be greatly appreciated!

Support h2c via Upgrade: header

As specified in RFC7540§3.2, allow HTTP/1.1 connections to upgrade to h2

Most of the plumbing is in place for this in the form of Bandit.DelegatingHandler. For examples of how to change protocols on the fly, see Bandit.InitialHandler's protocol sniffing implementation.

Bandit intercepting liveview send/2 messages

Hi again!

in our app, we are using liveview, and we are using send/2 and handl_info in :liveview

after switch to bandit, we been noticing that bandit is intercepting the handle_info call within the liveview

[error] GenServer #PID<0.5234.0> terminating
** (FunctionClauseError) no function clause matching in Bandit.HTTP1.Handler.handle_info/2
    (bandit 1.0.0-pre.3) lib/thousand_island/handler.ex:5: Bandit.HTTP1.Handler.handle_info({:update_user_view_count, "21691d84-2656-4556-9b02-f87c3f94bbbd"}, {%ThousandIsland.Socket{socket: #Port<0.216>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 60000, span: %ThousandIsland.Telemetry{span_name: :connection, telemetry_span_context: #Reference<0.2582905735.3298820105.108121>, start_time: -576455428605852008, start_metadata: %{parent_telemetry_span_context: #Reference<0.2582905735.3298820098.52671>, remote_address: {127, 0, 0, 1}, remote_port: 56410, telemetry_span_context: #Reference<0.2582905735.3298820105.108121>}}}, %{handler_module: Bandit.HTTP1.Handler, http_1_enabled: true, http_2_enabled: true, opts: %{http_1: [], http_2: [], websocket: []}, plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {HelioWeb.Endpoint, []}}, requests_processed: 10, websocket_enabled: true}})

here is how the code is being called in our liveview

  def mount(
        %{"id" => id},
        _session,
        %{assigns: %{current_user: current_user}} = socket
      ) do
    user = Accounts.get_by_username!(id)
    send(self(), {:update_user_view_count, user.id})

    {:ok,
     socket}
  end

  @impl true
  def handle_info({:update_user_view_count, user_id}, socket) do
    # Posts.update_post_field_count({:view, post_id})
    Helio.Posts.BatchCounter.Supervisor.increment_user({:view, user_id})

    {:noreply, socket}
  end

any guidance is appreciated

How to make bandit serve static assets using http2

Hello,

Thank you for making bandit. I am trying to create a simple server using Plug and wanted to serve static assets like js, css etc using http2.

Please advise how to do that.

I have this is my router (not using phoenix)
plug(Plug.Static,
at: "/",
from: :simple,
gzip: false,
only: ~w(assets fonts images favicon.ico robots.txt)
)

Thank you

No way to set `:inet6` option for the TCP transport

Basically same thing as mtrudel/thousand_island#25, but for Bandit -- Bandit.start_link/1 tries to use ThousandIsland's transport_options as a keyword list, even when it contains non-keyword options.

I'm trying to use Bandit with a Phoenix app, and pass :inet6 as the :inet.address_family option to the underlying socket. This is how I'm trying to configure the Phoenix endpoint:

config :admin, AdminWeb.Endpoint,
  adapter: Bandit.PhoenixAdapter,
  https: [
    port: 4040,
    thousand_island_options: [
      transport_options: [
        :inet6,
        cipher_suite: :strong,
        client_renegotiation: false,
        dhfile: "/etc/admin/ssl/dhparam.pem",
        cacertfile: "/etc/admin/ssl/ca.pem",
        certfile: "/etc/admin/ssl/cert.pem",
        keyfile: "/etc/admin/ssl/key.pem"
      ]
    ]
  ]

If I remove the :inet6 option, everything seems to work fine (except, as expected, the listening socket binds only to IPv4 addresses); when I include the :inet6 option, the following error occurs on startup:

** (Mix) Could not start application admin: Admin.Application.start(:normal, []) returned an error: shutdown: failed to start child: AdminWeb.Endpoint
    ** (EXIT) shutdown: failed to start child: {AdminWeb.Endpoint, :https}
        ** (EXIT) an exception was raised:
            ** (ArgumentError) expected a keyword list as the second argument, got: [:inet6, {:cipher_suite, :strong}, {:client_renegotiation, false}, {:dhfile, "/etc/admin/ssl/dhparam.pem"}, {:cacertfile, "/etc/admin/ssl/ca.pem"}, {:certfile, "/etc/admin/ssl/cert.pem"}, {:keyfile, "/etc/admin/ssl/key.pem"}]
                (elixir 1.14.4) lib/keyword.ex:995: Keyword.merge/2
                (bandit 0.7.7) lib/bandit.ex:342: Bandit.start_link/1
                (stdlib 4.3) supervisor.erl:414: :supervisor.do_start_child_i/3
                (stdlib 4.3) supervisor.erl:400: :supervisor.do_start_child/2
                (stdlib 4.3) supervisor.erl:384: anonymous fn/3 in :supervisor.start_children/2
                (stdlib 4.3) supervisor.erl:1250: :supervisor.children_map/4
                (stdlib 4.3) supervisor.erl:350: :supervisor.init_children/2
                (stdlib 4.3) gen_server.erl:851: :gen_server.init_it/2
                (stdlib 4.3) gen_server.erl:814: :gen_server.init_it/6
                (stdlib 4.3) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

Support for SSE

Hi,

Thanks for building a pure elixir HTTP server. Since the library is 100 % http/2 compliant, wondering if it includes the support for Server Side Events ?

Thanks in advance.

`Socket.close/1` called with nil

After running Greenbone OpenVAS (Vulnerability Scanner) against a bandit server, I noticed this error:

Screenshot 2022-11-17 at 13 45 02

I don't know the http request which lead to that error and couldn't quite figure out, what request leads to Socket.close/1 be called with a nil value.

@mtrudel Do you have any idea?

Phoenix LiveView doesn't handle server restart gracefully

Hi @mtrudel,

First off, bandit is really looking great, excellent work on the whole thing!

I filed a bug report here with LiveView, but Jose asked me to file an issue with bandit instead.

The basic run-down of what is happening is:

When the server is restarting, bandit closes the websocket connection with status code 1001 (meaning: server going away). LiveView, however, is interpreting this as "browser navigated to another page" and subsequently does not attempt to reconnect, leaving the user with a spinning cursor until they reload the page manually.

Returning a status code 1000 instead would solve the issues with Phoenix. I'm not sure what your take is on that though from the perspective of standard compliance.

WebSocket integration

Are there any concrete plans on how to integrate WebSockets?

Currently bandit doesn't do any routing and delegates everything to Plug (which doesn't support WebSockets (yet?)).

Will bandit handle the routing for the WebSockets or are there any plans on integrating WebSockets into the Plug.Router (either via adding it to Plug directly or injecting it via macro)?

invalid_request, CaseClauseError

I configured Bandit in a Phoenix 1.7-rc.0 / LiveView application. Ever since making the change, I'm getting invalid_request errors. In one application, this occurs periodically during deployments, in another one, it happens throughout the day, about 60 times per hour, which makes me think that it has something to do with health checks.

In any case, Cowboy does not log or raise errors under the same circumstances.

Before Bandit 0.6.4, this is what gets logged:

message
  GenServer #PID<0.7891.0> terminating
** (stop) :invalid_request
Last message: {:continue, :handle_connection}
metadata.domain
  otp
metadata.erl_level
  error
metadata.error.initial_call
  
 - 
metadata.error.reason
  {:invalid_request, []}
metadata.error_logger.report_cb
  &:gen_server.format_log/1
metadata.error_logger.tag
  error
severity
  error

After upgrading to Bandit 0.6.4, we're getting a CaseClauseError instead:

CaseClauseError: no case clause matching: {:error, :invalid_request}
  File "lib/thousand_island/handler.ex", line 387, in Bandit.DelegatingHandler.handle_continuation/2
  File "gen_server.erl", line 1123, in :gen_server.try_dispatch/4
  File "gen_server.erl", line 865, in :gen_server.loop/7
  File "proc_lib.erl", line 240, in :proc_lib.init_p_do_apply/3

I guess the :invalid_request error comes from here:

{:ok, {:http_error, _reason}, _rest} ->

This means that :erlang.decode_packet/3 returns an HTTP error, but unfortunately, Bandit discards the reason.

While we haven't figured out what exactly causes the invalid requests, this should probably be handled gracefully?

gRPC?

Hi, this is an excellent project as well as ThousandIsland, thanks for investing time in this.

I would like to know your opinion on how complex it would be to implement the gRPC protocol on top of this library. Do you think this would be viable? Do you have plans for this? What hooks would you need to implement to achieve this?

Implement support for HTTP trailers, upstream to Plug

As discussed at elixir-plug/plug#535 (comment), Plug lacks suppor for HTTP trailers. Beyond their usefulness in the real world, they could also help to tie off a couple of awkward spots when dealing with chunk encoding in HTTP/1 (as discussed on the linked thread above).

At least in h2, we already implement support for reading trailers, but currently just log and discard them.

Improve work with Plug project to improve URI canonicalization

As discussed at elixir-plug/plug#948 (comment), Plug itself should be responsible for canonicalizing URIs. It's a tricky problem with inevitable arbitrary decisions to be made, and these decisions should be centralized into Plug so that they're consistent from web server to web server. Along with this, the Plug.Conn.Adapter.conn/5 API should be updated to reflect the data needed for this (specifically, it shouldn't take a URI directly, but rather the constituent parts gathered from request / config / etc for Plug to sort through).

Missing common HTTP status codes

I have noticed that there are common HTTP status codes that Bandit is not aware of. For example, the very common 422 Unprocessable Entity returns "Unknown Status Code" from Bandit, but there are many others.

Is this a conscious choice, or would you accept a pull request adding some common, well-documented HTTP status codes?

At a minimum, I think it makes sense to add most of these codes from RFC 9110 and other places.

Exception when using System.cmd inside a plug function

After adding System.cmd("diff", ["file1.txt", "file2.txt"]) inside my handler I get the following exception:

16:49:36.864 [error] GenServer #PID<0.509.0> terminating
** (FunctionClauseError) no function clause matching in Bandit.HTTP1.Handler.handle_info/2
    (bandit 0.4.5) lib/thousand_island/handler.ex:5: Bandit.HTTP1.Handler.handle_info({:EXIT, #Port<0.35>, :normal}, {%ThousandIsland.Socket{acceptor_id: "3162ABC96567", connection_id: "7278E72B3A7D", socket: #Port<0.34>, transport_module: ThousandIsland.Transports.TCP}, %{handler_module: Bandit.HTTP1.Handler, plug: {ShowroomWeb.Router, []}, read_timeout: 60000}})
    (stdlib 3.16.1) gen_server.erl:695: :gen_server.try_dispatch/4
    (stdlib 3.16.1) gen_server.erl:771: :gen_server.handle_msg/6
    (stdlib 3.16.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message: {:EXIT, #Port<0.35>, :normal}
State: {%ThousandIsland.Socket{acceptor_id: "3162ABC96567", connection_id: "7278E72B3A7D", socket: #Port<0.34>, transport_module: ThousandIsland.Transports.TCP}, %{handler_module: Bandit.HTTP1.Handler, plug: {ShowroomWeb.Router, []}, read_timeout: 60000}}

I tried switching to Cowboy and there it works fine. Seems the process sends an exit message which is not handled properly.

Cannot start multiple Bandits as a Cowboy drop-in

I'm trying to replace Cowboy with Bandit. I have some test code that does something like this:

{:ok, _} = start_supervised({Plug.Cowboy, scheme: :http, plug: Foo, options: [port: 4040]})
{:ok, _} = start_supervised({Plug.Cowboy, scheme: :http, plug: Bar, options: [port: 4041]})

However, replacing Plug.Cowboy with Bandit results in:

** (MatchError) no match of right hand side value: {:error, {:duplicate_child_name, Bandit}}

This seems to be because Bandit uses Bandit as the child spec ID, but plug_cowboy generates the id from the plug and scheme.

This can be worked around by specifying a custom ID, however as Bandit is advertised as a drop-in replacement for Cowboy, generating a unique id would allow multiple servers to be supervised using the {Bandit, opts} child spec format.

Runtime error on special Phoenix.Endpoint usage

Hello! I tried to replace Cowboy with Bandit in a Phoenix app that uses Absinthe as a GraphQL implementation for our server.
I tried to debug it locally but the error raises sometimes and it’s always linked to our /graphql endpoint.

Here is our special Phoenix.Endpoint usage: We use different plug that halt the connexion before it reaches Phoenix.Router.

  # ...
  plug(Plug.MethodOverride)
  plug(Plug.Head)

  plug(MyAppHealth.Router)
  plug(MyAppGraphQL.Router)
  plug(:halt_if_sent)
  plug(MyAppWeb.Router)

  # Splitting routers in separate modules has a negative side effect:
  # Phoenix.Router does not check the Plug.Conn state and tries to match the
  # route even if it was already handled/sent by another router.
  defp halt_if_sent(%{state: :sent, halted: false} = conn, _opts), do: halt(conn)
  defp halt_if_sent(conn, _opts), do: conn

It does not always "crash" but it seems to be linked to telemetry events. It does not impact the user request. Here is the runtime error:

[mfa=Phoenix.Logger.phoenix_endpoint_start/4 ] [info] POST /graphql
[mfa=Phoenix.Logger.phoenix_endpoint_stop/4 ] [info] Sent 400 in 157µs
[mfa=Phoenix.Logger.phoenix_endpoint_start/4 ] [info] POST /graphql
[mfa=Phoenix.Logger.phoenix_endpoint_stop/4 ] [info] Sent 400 in 166µs
[mfa=Phoenix.Logger.phoenix_endpoint_start/4 ] [info] POST /graphql
[mfa=Phoenix.Logger.phoenix_endpoint_stop/4 ] [info] Sent 400 in 166µs
[mfa=Phoenix.Logger.phoenix_endpoint_start/4 ] [info] POST /graphql
[mfa=Phoenix.Logger.phoenix_endpoint_stop/4 ] [info] Sent 400 in 157µs
[mfa=MyAppGraphQL.Plugs.ErrorReporting.report_message/1 ] [error] [absinthe_resolution_error: [%{"message" => "No query document supplied"}]]
[mfa=:gen_server.error_info/8 ] [error] GenServer #PID<0.1896.0> terminating
** (FunctionClauseError) no function clause matching in Bandit.HTTP1.Handler.handle_info/2
    (bandit 0.6.9) lib/thousand_island/handler.ex:5: Bandit.HTTP1.Handler.handle_info({#Reference<0.2692061718.3879796743.144965>, :ok}, {%ThousandIsland.Socket{socket: #Port<0.55>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 15000, span: %ThousandIsland.Telemetry{span_name: :connection, span_id: "5IBONXXLYJSSSC2Y", start_time: -576460732887533691}}, %{handler_module: Bandit.HTTP1.Handler, plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {MyAppWeb.Endpoint, []}}}})
    (stdlib 4.0.1) gen_server.erl:1120: :gen_server.try_dispatch/4
    (stdlib 4.0.1) gen_server.erl:1197: :gen_server.handle_msg/6
    (stdlib 4.0.1) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: {#Reference<0.2692061718.3879796743.144965>, :ok}
State: {%ThousandIsland.Socket{socket: #Port<0.55>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 15000, span: %ThousandIsland.Telemetry{span_name: :connection, span_id: "5IBONXXLYJSSSC2Y", start_time: -576460732887533691}}, %{handler_module: Bandit.HTTP1.Handler, plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {MyAppWeb.Endpoint, []}}}}

The error never happens on normal Phoenix.Router defined routes.

Evaluate feasibility of improving post-GOAWAY behaviour

We're compliant with RFC7540§6.8, but we could consider doing a better job of continuing to process currently open streams after receiving a GOAWAY frame. Currently we just shut ourselves down upon receipt; it would be worth it to investigate the feasibility / value in improving this.

Client hangs on redirector plug app

Hello,

I was testing porting an app from cowboy2 to bandit and found a behaviour I am not sure on where it should be fixed.

My app is using Plug.Router (no phoenix) and one of the endpoints makes a 301 redirect, with a location header and an empty body.

A simple simulation of that can be:

  match _ do
    conn
    |> put_resp_header("location", "http://example.com")
    |> send_resp(301, "")
  end

Clients hang waiting for some indication that content is done, as in:

> curl -v http://localhost:4000/
# ...
< HTTP/1.1 301 Moved Permanently
< date: Wed, 02 Nov 2022 14:46:38 GMT
< cache-control: max-age=0, private, must-revalidate
< x-request-id: FyPL1BNQtEYMFM4AAQsD
< location: http://example.org/
* no chunk, no close, no size. Assume close to signal end

I figured it out that cowboy adds a content-length: 0 header making curl and other clients happy.

I fixed in my app by doing the same with a put_resp_header call. My question is: who should handle that case? Is my plug required to provide the content-length?

Telemetry compatibility

It looks like a lot of the telemetry in Bandit doesn't follow the pattern that :telemetry.span/3 sets. Why don't you use :telemetry.span/3? It handles some of the work you're doing yourself in Bandit, like creating span contexts and adding timestamps and durations. There are also some libraries that adapt telemetry to other protocols, and they expect data in the style of span/3. In particular, I'm thinking of the OpenTelemetry adapter library that turns telemetry spans into OpenTelemetry data.

Increasing latency as # of connections increase.

TL;DR;

I wanted to raise awareness to the non-linear increase in latency/TTFB as # of Connections increases (especially as # of connections >= 256). This isn’t typical of Erlang/OTP and might warrant further investigation.

Context:

A major characteristic of Erlang/OTP is to respond fast or not respond at all. Other languages might have higher RPS, but Erlang has the most consistent performance as measured in response time (which is flat and extremely low TTFB under load).

This is a major reason why people choose Erlang because they know under load, customer experience won’t be impacted since response time will always be fast.

Benchmarks:

You can see that desirable scaling curve play out in the below language benchmarks.

Example A, notice Cowboy 1.x consistently low/flat response times (red solid line):

Example B, notice Cowboy 1.x low response times (red solid line) vs 2.x:

The accompanying blog posts, where the graphs can be found:

Observation:

In the Bandit vs Cowboy benchmark, Bandits displays ever increasing latency (where Cowboy TTFB has a smaller variance) as # of a connections increase.

Essentially, Bandit is not scaling like typical Erlang/OTP, where it has consistently low TTFB under load.

  • On h2c / 16 streams, Bandit has upwards of 30x longer TTFB compared to Cowboy (Bandits taking multiple seconds to respond relative to Cowboy).

  • Note: even when Cowboy has errors, it’s still responding fast (low TTFB).

https://github.com/mtrudel/network_benchmark/blob/0b18a9b299b9619c38d2a70ab967831565121d65/benchmarks-09-2021.pdf

Ask:

Given that the 0.6x series is to work on performance - it might warrant investigation into how to achieve a more consistent low latency (fast TTFB) as # of Connections increase. Essentially, scale with regards to TTFB more similar to Cowboy 1.x, where TTFB remains consistently low. Hopefully this doesn’t come at the cost of RPS, which makes Bandit so great.

Please don’t take my comments as being negative. You’ve created a phenomenal web server, and want to express appreciation. So thank you in advance for all your great work on Bandit.

Bandit.HTTP1.Adapter.chunk/2 errors on non-string iodata `chunk` argument

Describe the bug
The implementation of the Plug.Conn.chunk callback in Bandit.HTTP1.Adapter uses byte_size/1 which only accepts bitstring().

The Plug.Conn.Adapter callback chunk expects body :: Conn.body() for the second argument, where @type body :: iodata.

** (ArgumentError) errors were found at the given arguments:                                                                                                            
                                                                                                                                                                        
  * 1st argument: not a bitstring                                                                                                                                       
                                                                                                                                                                        
This typically happens when calling Kernel.byte_size/1 with an invalid argument or when performing binary construction or binary concatenation with <> and one of the arguments is not a binary

It should be possible to resolve by replacing byte_size with IO.iodata_length here:
https://github.com/mtrudel/bandit/blob/main/lib/bandit/http1/adapter.ex#L298

I can confirm that this replacement eliminates the error in my codebase.

To Reproduce
Pass a list as the second argument for Plug.Conn.chunk/2 when using Bandit as an adapter with HTTP 1.

Expected behavior
Chunk sends the iodata without issue.

Runtime

  • Elixir version - 1.13.3
  • Erlang version - 25 [erts-13.0.2]
  • Bandit version - 0.6.2
  • Plug version - 1.14.0

Additional context
Add any other context about the problem here.

Could not determine a protocol - when running on EC2

Hi, just exploring this project to see if it's something we're able to use in a running application and seems ideal for performance. One thing that I haven't been able to work out is AWS status checks when deploying this to an EC2, as our logs fill with messages about not recognising the protocol.

{"report_cb":"&:gen_server.format_log/2","error_logger":{"tag":"error","report_cb":"&:gen_server.format_log/1"},"crash_reason":["Could not determine a protocol",[]],"metadata":{},"message":"GenServer #PID<0.1566.0> terminating\n** (stop) \"Could not determine a protocol\"\nLast message: {:continue, :handle_connection}\nState: {%ThousandIsland.Socket{acceptor_id: \"DA364D26BD40\", connection_id: \"302D04F1381E\", read_timeout: :infinity, socket: #Port<0.533>, transport_module: ThousandIsland.Transports.TCP}, %{handler_module: Bandit.InitialHandler, plug: {Web.Router, []}, read_timeout: 60000}}","level":"error","datetime":"2022-08-05T08:51:19.327125Z"}

I was seeing this error continuously in our application logs. I tried to trace the handler functions and when creating sockets, but the only thing I could see was a request I made got into "handle_data" in Bandit but the EC2 status checks didn't. I tried "handle_connection" but couldn't find anything common between them.

Any suggestions with this, or pointers in getting further with the tracing are appreciated.

This may be an issue for ThousandIsland and not Bandit, so happy to raise there if this is the case. The message was showing me the Bandit handler but ThousandIsland socket and looks to be from Bandit:

https://github.com/mtrudel/bandit/blob/main/lib/bandit/initial_handler.ex#L28

Gzip compression

In anticipation of v0.7 support for gzip compression, may I suggest that it is implemented in a way that goes one step further than Cowboy. Instead of a binary on/off option, Bandit could allow the user to set the gzip compression level, like Nginx and Caddy do, as follows for Phoenix:

config :prj, Endpoint, http(s): [gzip: 0-9]

The default :gzip value would be zero (no compression). There would be no pre-selected compression level, and that is a good thing. Nginx's default seems to be 1, Caddy's 6. The Bandit documentation could recommend a positive value or range (e.g. 3 or 1-6), but leave the actual decision to the user. In the future, there could also be other options, such as:

config :prj, Endpoint, http(s): [zstd: 0-22]

In case of conflict, e.g. http(s): [gzip: 2, zstd: 4], the obvious resolution would be to default to gzip.

I hope this helps.

Using as a GRPC server

Hey 👋🏼

I've been looking for a HTTP 2.0 server that I can use to build a GRPC server. I'm wondering if it's something that you have looked into?

For unary calls the way that Plug works is good enough. request comes and the server serves it.

However for server-streaming something like a genserver is optimal. What I mean is somehow making a request on a route, spawn a genserver and until the process decides that there is no more data need to be transferred.

Any suggestions or thoughts?

Connection Draining at Shutdown

Hey @mtrudel

I really like Bandit and want to start using it more. Now I've stumbled upon the topic of connection draining. Does something like Plug.Cowboy.Drainer exist for Bandit? Or are there plans to add it? Or is it something you'd see outside of the module?

Thanks,
Michael

Timeout issue in dev mode with phoenix

I'm not sure if this is the correct place to report this issue.

When working with a phoenix live view app in development I get this error reported.

[error] GenServer #PID<0.1040.0> terminating
** (stop) time out
Last message: :timeout
State: {%ThousandIsland.Socket{socket:

I think this is related to development mode because it references the SyncCodeReloadPlug

plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {WodWeb.Endpoint, []}}}, handler_module: Bandit.HTTP2.Handler, plug: {Phoenix.Endpoint.SyncCodeReloadPlug, {WodWeb.Endpoint, []}}}}

It may be something bespoke to my setup because I'm running HTTPS in dev.

Happy to provide more information or move to a different place if it's the wrong place to report

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.