Giter VIP home page Giter VIP logo

bandit's People

Contributors

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 liamwhite avatar meeq 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

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

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

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.

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.

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.

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).

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.

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}

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.

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.

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?

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?

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!

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.

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
...

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.

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?

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

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)?

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.

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

[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

`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.

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]

`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?

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!

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

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!

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.

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?

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.

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

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

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

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.

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?

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.

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.

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.