diff options
-rw-r--r-- | Makefile | 28 | ||||
-rw-r--r-- | examples/echo_post/src/toppage_handler.erl | 6 | ||||
-rw-r--r-- | guide/handlers.md | 22 | ||||
-rw-r--r-- | guide/hooks.md | 69 | ||||
-rw-r--r-- | guide/http_handlers.md | 22 | ||||
-rw-r--r-- | guide/internals.md | 76 | ||||
-rw-r--r-- | guide/introduction.md | 2 | ||||
-rw-r--r-- | guide/loop_handlers.md | 30 | ||||
-rw-r--r-- | guide/req.md | 180 | ||||
-rw-r--r-- | guide/routing.md | 89 | ||||
-rw-r--r-- | guide/static_handlers.md | 18 | ||||
-rw-r--r-- | guide/toc.md | 14 | ||||
-rw-r--r-- | guide/ws_handlers.md | 25 | ||||
-rw-r--r-- | src/cowboy_bstr.erl | 49 | ||||
-rw-r--r-- | src/cowboy_protocol.erl | 4 | ||||
-rw-r--r-- | src/cowboy_req.erl | 22 | ||||
-rw-r--r-- | src/cowboy_rest.erl | 6 | ||||
-rw-r--r-- | src/cowboy_websocket.erl | 8 | ||||
-rw-r--r-- | test/http_SUITE.erl | 40 | ||||
-rw-r--r-- | test/http_handler_echo_body.erl | 4 |
20 files changed, 621 insertions, 93 deletions
@@ -4,6 +4,9 @@ PROJECT = cowboy RANCH_VSN = 0.6.0 ERLC_OPTS = -Werror +debug_info +warn_export_all # +bin_opt_info +warn_missing_spec +DEPS_DIR ?= $(CURDIR)/deps +export DEPS_DIR + .PHONY: all clean-all app clean docs clean-docs tests autobahn build-plt dialyze # Application. @@ -12,25 +15,26 @@ all: app clean-all: clean clean-docs rm -f .$(PROJECT).plt - rm -rf deps logs + rm -rf $(DEPS_DIR) logs deps/ranch: - @mkdir -p deps/ - git clone -n -- https://github.com/extend/ranch.git deps/ranch - cd deps/ranch ; git checkout -q $(RANCH_VSN) + @mkdir -p $(DEPS_DIR) + git clone -n -- https://github.com/extend/ranch.git $(DEPS_DIR)/ranch + cd $(DEPS_DIR)/ranch ; git checkout -q $(RANCH_VSN) MODULES = $(shell ls src/*.erl | sed 's/src\///;s/\.erl/,/' | sed '$$s/.$$//') app: deps/ranch - @cd deps/ranch ; make + @$(MAKE) -C $(DEPS_DIR)/ranch @mkdir -p ebin/ - @cat src/cowboy.app.src \ + @cat src/$(PROJECT).app.src \ | sed 's/{modules, \[\]}/{modules, \[$(MODULES)\]}/' \ - > ebin/cowboy.app - erlc -v $(ERLC_OPTS) -o ebin/ -pa ebin/ src/cowboy_middleware.erl src/*.erl + > ebin/$(PROJECT).app + erlc -v $(ERLC_OPTS) -o ebin/ -pa ebin/ \ + src/$(PROJECT)_middleware.erl src/*.erl clean: - -@cd deps/ranch && make clean + -@$(MAKE) -C $(DEPS_DIR)/ranch clean rm -rf ebin/ rm -f test/*.beam rm -f erl_crash.dump @@ -38,7 +42,7 @@ clean: # Documentation. docs: clean-docs - erl -noshell -eval 'edoc:application(cowboy, ".", []), init:stop().' + erl -noshell -eval 'edoc:application($(PROJECT), ".", []), init:stop().' clean-docs: rm -f doc/*.css @@ -49,7 +53,7 @@ clean-docs: # Tests. CT_RUN = ct_run \ - -pa ebin deps/*/ebin \ + -pa ebin $(DEPS_DIR)/*/ebin \ -dir test \ -logdir logs \ -cover test/cover.spec @@ -67,7 +71,7 @@ autobahn: clean app build-plt: app @dialyzer --build_plt --output_plt .$(PROJECT).plt \ - --apps erts kernel stdlib sasl inets crypto public_key ssl deps/ranch + --apps erts kernel stdlib crypto public_key ssl $(DEPS_DIR)/ranch dialyze: @dialyzer --src src --plt .$(PROJECT).plt --no_native \ diff --git a/examples/echo_post/src/toppage_handler.erl b/examples/echo_post/src/toppage_handler.erl index 808ba8e..21e1dc6 100644 --- a/examples/echo_post/src/toppage_handler.erl +++ b/examples/echo_post/src/toppage_handler.erl @@ -12,9 +12,9 @@ init(_Transport, Req, []) -> handle(Req, State) -> {Method, Req2} = cowboy_req:method(Req), - {HasBody, Req3} = cowboy_req:has_body(Req2), - {ok, Req4} = maybe_echo(Method, HasBody, Req3), - {ok, Req4, State}. + HasBody = cowboy_req:has_body(Req2), + {ok, Req3} = maybe_echo(Method, HasBody, Req2), + {ok, Req3, State}. maybe_echo(<<"POST">>, true, Req) -> {ok, PostVals, Req2} = cowboy_req:body_qs(Req), diff --git a/guide/handlers.md b/guide/handlers.md index dac5460..e2c1264 100644 --- a/guide/handlers.md +++ b/guide/handlers.md @@ -33,7 +33,23 @@ init(_Any, _Req, _Opts) -> {upgrade, protocol, my_protocol}. ``` -The `my_protocol` module will be used for further processing of the -request. It requires only one callback, `upgrade/4`. +Cowboy comes with two protocol upgrades: `cowboy_rest` and +`cowboy_websocket`. Use these values in place of `my_protocol` +to use them. -@todo Describe `upgrade/4` when the middleware code gets pushed. +Custom protocol upgrades +------------------------ + +The `my_protocol` module above will be used for further processing +of the request. It requires only one callback, `upgrade/4`. + +It receives the request object, the middleware environment, and +the handler this request has been routed to along with its options. + +``` erlang +upgrade(Req, Env, Handler, HandlerOpts) -> + %% ... +``` + +This callback is expected to behave like any middleware. Please +see the corresponding chapter for more information. diff --git a/guide/hooks.md b/guide/hooks.md index ba48c4a..d4b520a 100644 --- a/guide/hooks.md +++ b/guide/hooks.md @@ -4,9 +4,74 @@ Hooks On request ---------- -@todo Describe. +The `onrequest` hook is called as soon as Cowboy finishes fetching +the request headers. It occurs before any other processing, including +routing. It can be used to perform any modification needed on the +request object before continuing with the processing. If a reply is +sent inside this hook, then Cowboy will move on to the next request, +skipping any subsequent handling. + +This hook is a function that takes a request object as argument, +and returns a request object. This function MUST NOT crash. Cowboy +will not send any reply if a crash occurs in this function. + +You can specify the `onrequest` hook when creating the listener, +inside the request options. + +``` erlang +cowboy:start_http(my_http_listener, 100, + [{port, 8080}], + [ + {env, [{dispatch, Dispatch}]}, + {onrequest, fun ?MODULE:debug_hook/1} + ] +). +``` + +The following hook function prints the request object everytime a +request is received. This can be useful for debugging, for example. + +``` erlang +debug_hook(Req) -> + erlang:display(Req), + Req. +``` + +Make sure to always return the last request object obtained. On response ----------- -@todo Describe. +The `onresponse` hook is called right before sending the response +to the socket. It can be used for the purposes of logging responses, +or for modifying the response headers or body. The best example is +providing custom error pages. + +Note that like the `onrequest` hook, this function MUST NOT crash. +Cowboy may or may not send a reply if this function crashes. + +You can specify the `onresponse` hook when creating the listener also. + +``` erlang +cowboy:start_http(my_http_listener, 100, + [{port, 8080}], + [ + {env, [{dispatch, Dispatch}]}, + {onresponse, fun ?MODULE:custom_404_hook/4} + ] +). +``` + +The following hook function will provide a custom body for 404 errors +when it has not been provided before, and will let Cowboy proceed with +the default response otherwise. + +``` erlang +custom_404_hook(404, Headers, <<>>, Req) -> + {ok, Req2} = cowboy_req:reply(404, Headers, <<"404 Not Found.">>, Req), + Req2; +custom_404_hook(_, _, _, Req) -> + Req. +``` + +Again, make sure to always return the last request object obtained. diff --git a/guide/http_handlers.md b/guide/http_handlers.md index 0d8886d..ea88c79 100644 --- a/guide/http_handlers.md +++ b/guide/http_handlers.md @@ -6,12 +6,22 @@ Purpose HTTP handlers are the simplest Cowboy module to handle a request. -Callbacks ---------- - -@todo Describe the callbacks. - Usage ----- -@todo Explain how to use them. +You need to implement three callbacks for HTTP handlers. The first, +`init/3`, is common to all handlers. In the context of HTTP handlers +this should be used for any initialization needs. + +The second callback, `handle/2`, is where most of your code should +be. As the name explains, this is where you handle the request. + +The last callback, `terminate/2`, will be empty most of the time. +It's used for any needed cleanup. If you used the process dictionary, +timers, monitors then you most likely want to stop them in this +callback, as Cowboy might end up reusing this process for subsequent +requests. Please see the Internals chapter for more information. + +Of course the general advice is to not use the process dictionary, +and that any operation requiring reception of messages should be +done in a loop handler, documented in its own chapter. diff --git a/guide/internals.md b/guide/internals.md index 431ca01..0f8adc2 100644 --- a/guide/internals.md +++ b/guide/internals.md @@ -4,10 +4,76 @@ Internals Architecture ------------ -@todo Describe. +Cowboy is a lightweight HTTP server. -Efficiency considerations -------------------------- +It is built on top of Ranch. Please see the Ranch guide for more +informations. -@todo Mention that you need to cleanup in terminate especially if you -used the process dictionary, started timers, started monitoring... +It uses only one process per connection. The process where your +code runs is the process controlling the socket. Using one process +instead of two allows for lower memory usage. + +It uses binaries. Binaries are more efficient than lists for +representing strings because they take less memory space. Processing +performance can vary depending on the operation. Binaries are known +for generally getting a great boost if the code is compiled natively. +Please see the HiPE documentation for more details. + +Because querying for the current date and time can be expensive, +Cowboy generates one `Date` header value every second, shares it +to all other processes, which then simply copy it in the response. +This allows compliance with HTTP/1.1 with no actual performance loss. + +One process for many requests +----------------------------- + +As previously mentioned, Cowboy only use one process per connection. +Because there can be more than one request per connection with the +keepalive feature of HTTP/1.1, that means the same process will be +used to handle many requests. + +Because of this, you are expected to make sure your process cleans +up before terminating the handling of the current request. This may +include cleaning up the process dictionary, timers, monitoring and +more. + +Lowercase header names +---------------------- + +For consistency reasons it has been chosen to convert all header names +to lowercase binary strings. This prevents the programmer from making +case mistakes, and is possible because header names are case insensitive. + +This works fine for the large majority of clients. However, some badly +implemented clients, especially ones found in corporate code or closed +source products, may not handle header names in a case insensitive manner. +This means that when Cowboy returns lowercase header names, these clients +will not find the headers they are looking for. + +A simple way to solve this is to create an `onresponse` hook that will +format the header names with the expected case. + +``` erlang +capitalize_hook(Status, Headers, Body, Req) -> + Headers2 = [{cowboy_bstr:capitalize_token(N), V} + || {N, V} <- Headers], + {ok, Req2} = cowboy_req:reply(Status, Headers2, Body, Req), + Req2. +``` + +Improving performance +--------------------- + +By default the maximum number of active connections is set to a +generally accepted big enough number. This is meant to prevent having +too many processes performing potentially heavy work and slowing +everything else down, or taking up all the memory. + +Disabling this feature, by setting the `{max_connections, infinity}` +protocol option, would give you greater performance when you are +only processing short-lived requests. + +Another option is to define platform-specific socket options that +are known to improve their efficiency. + +Please see the Ranch guide for more information. diff --git a/guide/introduction.md b/guide/introduction.md index 4d1dacc..f1fd18e 100644 --- a/guide/introduction.md +++ b/guide/introduction.md @@ -77,7 +77,7 @@ Dispatch = [ ], %% Name, NbAcceptors, TransOpts, ProtoOpts cowboy:start_http(my_http_listener, 100, - [{port, 8080}], + [{port, 8080}], [{env, [{dispatch, Dispatch}]}] ). ``` diff --git a/guide/loop_handlers.md b/guide/loop_handlers.md index 6d67c62..64cf80a 100644 --- a/guide/loop_handlers.md +++ b/guide/loop_handlers.md @@ -16,15 +16,23 @@ While the same can be accomplished using plain HTTP handlers, it is recommended to use loop handlers because they are well-tested and allow using built-in features like hibernation and timeouts. -Callbacks ---------- - -@todo Describe the callbacks. - Usage ----- -@todo Explain how to use them. +Loop handlers are used for requests where a response might not +be immediately available, but where you would like to keep the +connection open for a while in case the response arrives. The +most known example of such practice is known as long-polling. + +Loop handlers can also be used for requests where a response is +partially available and you need to stream the response body +while the connection is open. The most known example of such +practice is known as server-sent events. + +Loop handlers essentially wait for one or more Erlang messages +and feed these messages to the `info/3` callback. It also features +the `init/3` and `terminate/2` callbacks which work the same as +for plain HTTP handlers. The following handler waits for a message `{reply, Body}` before sending a response. If this message doesn't arrive within 60 @@ -41,14 +49,14 @@ this message. -export([terminate/2]). init({tcp, http}, Req, Opts) -> - {loop, Req, undefined_state, 60000, hibernate}. + {loop, Req, undefined_state, 60000, hibernate}. info({reply, Body}, Req, State) -> - {ok, Req2} = cowboy_req:reply(200, [], Body, Req), - {ok, Req2, State}; + {ok, Req2} = cowboy_req:reply(200, [], Body, Req), + {ok, Req2, State}; info(Message, Req, State) -> - {loop, Req, State, hibernate}. + {loop, Req, State, hibernate}. terminate(Req, State) -> - ok. + ok. ``` diff --git a/guide/req.md b/guide/req.md index 79c59a9..c039658 100644 --- a/guide/req.md +++ b/guide/req.md @@ -4,19 +4,187 @@ Request object Purpose ------- -@todo Describe. +The request object is a special variable that can be used +to interact with a request, extracting information from it +or modifying it, and sending a response. + +It's a special variable because it contains both immutable +and mutable state. This means that some operations performed +on the request object will always return the same result, +while others will not. For example, obtaining request headers +can be repeated safely. Obtaining the request body can only +be done once, as it is read directly from the socket. + +With few exceptions, all calls to the `cowboy_req` module +will return an updated request object. You MUST use the new +request object instead of the old one for all subsequent +operations. Request ------- -@todo Describe. +Cowboy allows you to retrieve a lot of information about +the request. All these calls return a `{Value, Req}` tuple, +with `Value` the requested value and `Req` the updated +request object. + +The following access functions are defined in `cowboy_req`: + + * `method/1`: the request method (`<<"GET">>`, `<<"POST">>`...) + * `version/1`: the HTTP version (`{1,0}` or `{1,1}`) + * `peer/1`: the peer address and port number + * `peer_addr/1`: the peer address guessed using the request headers + * `host/1`: the hostname requested + * `host_info/1`: the result of the `[...]` match on the host + * `port/1`: the port number used for the connection + * `path/1`: the path requested + * `path_info/1`: the result of the `[...]` match on the path + * `qs/1`: the entire query string unmodified + * `qs_val/{2,3}`: the value for the requested query string key + * `qs_vals/1`: all key/values found in the query string + * `fragment/1`: the fragment part of the URL (e.g. `#nav-links`) + * `host_url/1`: the requested URL without the path, qs and fragment + * `url/1`: the requested URL + * `binding/{2,3}`: the value for the requested binding found during routing + * `bindings/1`: all key/values found during routing + * `header/{2,3}`: the value for the requested header name + * `headers/1`: all headers name/value + * `cookie/{2,3}`: the value for the requested cookie name + * `cookies/1`: all cookies name/value + * `meta/{2,3}`: the meta information for the requested key + +All the functions above that can take two or three arguments +take an optional third argument for the default value if +none is found. Otherwise it will return `undefined`. + +In addition, Cowboy allows you to parse headers using the +`parse_header/{2,3}` function, which takes a header name +as lowercase binary, the request object, and an optional +default value. It returns `{ok, ParsedValue, Req}` if it +could be parsed, `{undefined, RawValue, Req}` if Cowboy +doesn't know this header, and `{error, badarg}` if Cowboy +encountered an error while trying to parse it. + +Finally, Cowboy allows you to set request meta information +using the `set_meta/3` function, which takes a name, a value +and the request object and returns the latter modified. Request body ------------ -@todo Describe. +Cowboy will not read the request body until you ask it to. +If you don't, then Cowboy will simply discard it. It will +not take extra memory space until you start reading it. + +Cowboy has a few utility functions for dealing with the +request body. + +The function `has_body/1` will return whether the request +contains a body. Note that some clients may not send the +right headers while still sending a body, but as Cowboy has +no way of detecting it this function will return `false`. + +The function `body_length/1` retrieves the size of the +request body. If the body is compressed, the value returned +here is the compressed size. If a `Transfer-Encoding` header +was passed in the request, then Cowboy will return a size +of `undefined`, as it has no way of knowing it. + +If you know the request contains a body, and that it is +of appropriate size, then you can read it directly with +either `body/1` or `body_qs/1`. Otherwise, you will want +to stream it with `stream_body/1` and `skip_body/1`, with +the streaming process optionally initialized using `init_stream/4`. + +Multipart request body +---------------------- + +Cowboy provides facilities for dealing with multipart bodies. +They are typically used for uploading files. You can use two +functions to process these bodies, `multipart_data/1` and +`multipart_skip/1`. + +Response +-------- + +You can send a response by calling the `reply/{2,3,4}` function. +It takes the status code for the response (usually `200`), +an optional list of headers, an optional body and the request +object. + +The following snippet sends a simple response with no headers +specified but with a body. + +``` erlang +{ok, Req2} = cowboy_req:reply(200, [], "Hello world!", Req). +``` + +If this is the only line in your handler then make sure to return +the `Req2` variable to Cowboy so it can know you replied. + +If you want to send HTML you'll need to specify the `Content-Type` +header so the client can properly interpret it. + +``` erlang +{ok, Req2} = cowboy_req:reply(200, + [{<<"content-type">>, <<"text/html">>}], + "<html><head>Hello world!</head><body><p>Hats off!</p></body></html>", + Req). +``` + +You only need to make sure to follow conventions and to use a +lowercase header name. + +Chunked response +---------------- + +You can also send chunked responses using `chunked_reply/{2,3}`. +Chunked responses allow you to send the body in chunks of various +sizes. It is the recommended way of performing streaming if the +client supports it. + +You must first initiate the response by calling the aforementioned +function, then you can call `chunk/2` as many times as needed. +The following snippet sends a body in three chunks. + +``` erlang +{ok, Req2} = cowboy_req:chunked_reply(200, Req), +ok = cowboy_req:chunk("Hello...", Req2), +ok = cowboy_req:chunk("chunked...", Req2), +ok = cowboy_req:chunk("world!!", Req2). +``` + +As you can see the call to `chunk/2` does not return a modified +request object. It may return an error, however, so you should +make sure that you match the return value on `ok`. + +Response preconfiguration +------------------------- + +Cowboy allows you to set response cookies, headers or body +in advance without having to send the response at the same time. +Then, when you decide to send it, all these informations will be +built into the resulting response. + +Some of the functions available for this purpose also give you +additional functionality, like `set_resp_cookie/4` which will build +the appropriate `Set-Cookie` header, or `set_resp_body_fun/{2,3}` +which allows you to stream the response body. + +Note that any value given directly to `reply/{2,3,4}` will +override all preset values. This means for example that you +can set a default body and then override it when you decide +to send a reply. + +Reducing the memory footprint +----------------------------- -Reply ------ +When you are done reading information from the request object +and know you are not going to access it anymore, for example +when using long-polling or Websocket, you can use the `compact/1` +function to remove most of the data from the request object and +free memory. -@todo Describe. +``` erlang +Req2 = cowboy_req:compact(Req). +``` diff --git a/guide/routing.md b/guide/routing.md index 7d6fa41..2970b39 100644 --- a/guide/routing.md +++ b/guide/routing.md @@ -90,10 +90,12 @@ PathMatch3 = "/path/to/resource/". ``` Hosts with and without a trailing dot are equivalent for routing. +Similarly, hosts with and without a leading dot are also equivalent. ``` erlang HostMatch1 = "cowboy.example.org". HostMatch2 = "cowboy.example.org.". +HostMatch3 = ".cowboy.example.org". ``` It is possible to extract segments of the host and path and to store @@ -115,16 +117,93 @@ segment value where they were defined. For example, the URL `http://test.example.org/hats/wild_cowboy_legendary/prices` will result in having the value `test` bound to the name `subdomain` and the value `wild_cowboy_legendary` bound to the name `hat_name`. -They can later be retrieved using `cowboy_req:binding/{2,3}`. +They can later be retrieved using `cowboy_req:binding/{2,3}`. The +binding name must be given as an atom. -@todo special binding `'_'` -@todo optional path or segments -@todo same binding twice (+ optional + host/path) +There is a special binding name you can use to mimic the underscore +variable in Erlang. Any match against the `_` binding will succeed +but the data will be discarded. This is especially useful for +matching against many domain names in one go. + +``` erlang +HostMatch = "ninenines.:_". +``` + +Similarly, it is possible to have optional segments. Anything +between brackets is optional. + +``` erlang +PathMatch = "/hats/[page/:number]". +HostMatch = "[www.]ninenines.eu". +``` + +You can also have imbricated optional segments. + +``` erlang +PathMatch = "/hats/[page/[:number]]". +``` + +You can retrieve the rest of the host or path using `[...]`. +In the case of hosts it will match anything before, in the case +of paths anything after the previously matched segments. It is +a special case of optional segments, in that it can have +zero, one or many segments. You can then find the segments using +`cowboy_req:host_info/1` and `cowboy_req:path_info/1` respectively. +They will be represented as a list of segments. + +``` erlang +PathMatch = "/hats/[...]". +HostMatch = "[...]ninenines.eu". +``` + +Finally, if a binding appears twice in the routing rules, then the +match will succeed only if they share the same value. This copies +the Erlang pattern matching behavior. + +``` erlang +PathMatch = "/hats/:name/:name". +``` + +This is also true when an optional segment is present. In this +case the two values must be identical only if the segment is +available. + +``` erlang +PathMatch = "/hats/:name/[:name]". +``` + +If a binding is defined in both the host and path, then they must +also share the same value. + +``` erlang +PathMatch = "/:user/[...]". +HostMatch = ":user.github.com". +``` Constraints ----------- -@todo Describe constraints. +After the matching has completed, the resulting bindings can be tested +against a set of constraints. The match will succeed only if they all +succeed. + +They are always given as a two or three elements tuple, where the first +element is the name of the binding, the second element is the constraint's +name, and the optional third element is the constraint's arguments. + +The following constraints are currently defined: + + * {Name, int} + * {Name, function, (fun(Value) -> true | {true, NewValue} | false)} + +The `int` constraint will check if the binding is a binary string +representing an integer, and if it is, will convert the value to integer. + +The `function` constraint will pass the binding value to a user specified +function that receives the binary value as its only argument and must +return whether it fulfills the constraint, optionally modifying the value. + +Note that constraint functions SHOULD be pure and MUST NOT crash. Compilation ----------- diff --git a/guide/static_handlers.md b/guide/static_handlers.md index 5c897dd..f87515a 100644 --- a/guide/static_handlers.md +++ b/guide/static_handlers.md @@ -11,4 +11,20 @@ proper cache handling. Usage ----- -@todo Describe. +Static handlers are pre-written REST handlers. They only need +to be specified in the routing information with the proper options. + +The following example routing serves all files found in the +`priv_dir/static/` directory of the application. It uses a +mimetypes library to figure out the files' content types. + +``` erlang +Dispatch = [ + {'_', [ + {['...'], cowboy_static, [ + {directory, {priv_dir, static, []}}, + {mimetypes, {fun mimetypes:path_to_mimes/2, default}} + ]} + ]} +]. +``` diff --git a/guide/toc.md b/guide/toc.md index 2498b4d..2890172 100644 --- a/guide/toc.md +++ b/guide/toc.md @@ -15,17 +15,15 @@ Cowboy User Guide * [Handlers](handlers.md) * Purpose * Protocol upgrades + * Custom protocol upgrades * [HTTP handlers](http_handlers.md) * Purpose - * Callbacks * Usage * [Loop handlers](loop_handlers.md) * Purpose - * Callbacks * Usage * [Websocket handlers](ws_handlers.md) * Purpose - * Callbacks * Usage * [REST handlers](rest_handlers.md) * Purpose @@ -39,7 +37,11 @@ Cowboy User Guide * Purpose * Request * Request body - * Reply + * Multipart request body + * Response + * Chunked response + * Response preconfiguration + * Reducing the memory footprint * [Hooks](hooks.md) * On request * On response @@ -51,4 +53,6 @@ Cowboy User Guide * Handler middleware * [Internals](internals.md) * Architecture - * Efficiency considerations + * One process for many requests + * Lowercase header names + * Improving performance diff --git a/guide/ws_handlers.md b/guide/ws_handlers.md index 226ada0..c1e551e 100644 --- a/guide/ws_handlers.md +++ b/guide/ws_handlers.md @@ -16,15 +16,28 @@ is implemented by most browsers today, although for backward compatibility reasons a solution like [Bullet](https://github.com/extend/bullet) might be preferred. -Callbacks ---------- - -@todo Describe the callbacks. - Usage ----- -@todo Explain how to use them. +Websocket handlers are a bridge between the client and your system. +They can receive data from the client, through `websocket_handle/3`, +or from the system, through `websocket_info/3`. It is up to the +handler to decide to process this data, and optionally send a reply +to the client. + +The first thing to do to be able to handle websockets is to tell +Cowboy that it should upgrade the connection to use the Websocket +protocol, as follow. + +``` erlang +init({tcp, http}, Req, Opts) -> + {upgrade, protocol, cowboy_websocket}. +``` + +Cowboy will then switch the protocol and call `websocket_init`, +followed by zero or more calls to `websocket_data` and +`websocket_info`. Then, when the connection is shutting down, +`websocket_terminate` will be called. The following handler sends a message every second. It also echoes back what it receives. diff --git a/src/cowboy_bstr.erl b/src/cowboy_bstr.erl index e906de7..bc6818f 100644 --- a/src/cowboy_bstr.erl +++ b/src/cowboy_bstr.erl @@ -16,16 +16,40 @@ -module(cowboy_bstr). %% Binary strings. +-export([capitalize_token/1]). -export([to_lower/1]). %% Characters. -export([char_to_lower/1]). -export([char_to_upper/1]). +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-endif. + +%% @doc Capitalize a token. +%% +%% The first letter and all letters after a dash are capitalized. +%% This is the form seen for header names in the HTTP/1.1 RFC and +%% others. Note that using this form isn't required, as header name +%% are case insensitive, and it is only provided for use with eventual +%% badly implemented clients. +-spec capitalize_token(B) -> B when B::binary(). +capitalize_token(B) -> + capitalize_token(B, true, <<>>). +capitalize_token(<<>>, _, Acc) -> + Acc; +capitalize_token(<< $-, Rest/bits >>, _, Acc) -> + capitalize_token(Rest, true, << Acc/binary, $- >>); +capitalize_token(<< C, Rest/bits >>, true, Acc) -> + capitalize_token(Rest, false, << Acc/binary, (char_to_upper(C)) >>); +capitalize_token(<< C, Rest/bits >>, false, Acc) -> + capitalize_token(Rest, false, << Acc/binary, (char_to_lower(C)) >>). + %% @doc Convert a binary string to lowercase. --spec to_lower(binary()) -> binary(). -to_lower(L) -> - << << (char_to_lower(C)) >> || << C >> <= L >>. +-spec to_lower(B) -> B when B::binary(). +to_lower(B) -> + << << (char_to_lower(C)) >> || << C >> <= B >>. %% @doc Convert [A-Z] characters to lowercase. %% @end @@ -88,3 +112,22 @@ char_to_upper($x) -> $X; char_to_upper($y) -> $Y; char_to_upper($z) -> $Z; char_to_upper(Ch) -> Ch. + +%% Tests. + +-ifdef(TEST). + +capitalize_token_test_() -> + %% {Header, Result} + Tests = [ + {<<"heLLo-woRld">>, <<"Hello-World">>}, + {<<"Sec-Websocket-Version">>, <<"Sec-Websocket-Version">>}, + {<<"Sec-WebSocket-Version">>, <<"Sec-Websocket-Version">>}, + {<<"sec-websocket-version">>, <<"Sec-Websocket-Version">>}, + {<<"SEC-WEBSOCKET-VERSION">>, <<"Sec-Websocket-Version">>}, + {<<"Sec-WebSocket--Version">>, <<"Sec-Websocket--Version">>}, + {<<"Sec-WebSocket---Version">>, <<"Sec-Websocket---Version">>} + ], + [{H, fun() -> R = capitalize_token(H) end} || {H, R} <- Tests]. + +-endif. diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl index 0e9982b..a0f571b 100644 --- a/src/cowboy_protocol.erl +++ b/src/cowboy_protocol.erl @@ -30,7 +30,7 @@ %% <dt>max_headers</dt><dd>Max number of headers allowed. %% Defaults to 100.</dd> %% <dt>max_keepalive</dt><dd>Max number of requests allowed in a single -%% keep-alive session. Defaults to infinity.</dd> +%% keep-alive session. Defaults to 100.</dd> %% <dt>max_request_line_length</dt><dd>Max length allowed for the request %% line. Defaults to 4096.</dd> %% <dt>middlewares</dt><dd>The list of middlewares to execute when a @@ -107,7 +107,7 @@ init(ListenerPid, Socket, Transport, Opts) -> MaxHeaderNameLength = get_value(max_header_name_length, Opts, 64), MaxHeaderValueLength = get_value(max_header_value_length, Opts, 4096), MaxHeaders = get_value(max_headers, Opts, 100), - MaxKeepalive = get_value(max_keepalive, Opts, infinity), + MaxKeepalive = get_value(max_keepalive, Opts, 100), MaxRequestLineLength = get_value(max_request_line_length, Opts, 4096), Middlewares = get_value(middlewares, Opts, [cowboy_router, cowboy_handler]), Env = [{listener, ListenerPid}|get_value(env, Opts, [])], diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 89758dd..7f7ef32 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -163,7 +163,8 @@ | {non_neg_integer(), resp_body_fun()}, %% Functions. - onresponse = undefined :: undefined | cowboy_protocol:onresponse_fun() + onresponse = undefined :: undefined | already_called + | cowboy_protocol:onresponse_fun() }). -opaque req() :: #http_req{}. @@ -555,11 +556,10 @@ set_meta(Name, Value, Req=#http_req{meta=Meta}) -> %% Request Body API. %% @doc Return whether the request message has a body. --spec has_body(Req) -> {boolean(), Req} when Req::req(). +-spec has_body(cowboy_req:req()) -> boolean(). has_body(Req) -> - Has = lists:keymember(<<"content-length">>, 1, Req#http_req.headers) orelse - lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers), - {Has, Req}. + lists:keymember(<<"content-length">>, 1, Req#http_req.headers) orelse + lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers). %% @doc Return the request message body length, if known. %% @@ -728,7 +728,6 @@ skip_body(Req) -> %% @doc Return the full body sent with the request, parsed as an %% application/x-www-form-urlencoded string. Essentially a POST query string. -%% @todo We need an option to limit the size of the body for QS too. -spec body_qs(Req) -> {ok, [{binary(), binary() | true}], Req} | {error, atom()} when Req::req(). @@ -765,7 +764,6 @@ multipart_data(Req=#http_req{multipart={Length, Cont}}) -> multipart_data(Req=#http_req{body_state=done}) -> {eof, Req}. -%% @todo Typespecs. multipart_data(Req, Length, {headers, Headers, Cont}) -> {headers, Headers, Req#http_req{multipart={Length, Cont}}}; multipart_data(Req, Length, {body, Data, Cont}) -> @@ -868,6 +866,8 @@ has_resp_header(Name, #http_req{resp_headers=RespHeaders}) -> %% @doc Return whether a body has been set for the response. -spec has_resp_body(req()) -> boolean(). +has_resp_body(#http_req{resp_body=RespBody}) when is_function(RespBody) -> + true; has_resp_body(#http_req{resp_body={Length, _}}) -> Length > 0; has_resp_body(#http_req{resp_body=RespBody}) -> @@ -1163,13 +1163,17 @@ to_list(Req) -> response(Status, Headers, RespHeaders, DefaultHeaders, Body, Req=#http_req{ socket=Socket, transport=Transport, version=Version, pid=ReqPid, onresponse=OnResponse}) -> - FullHeaders = response_merge_headers(Headers, RespHeaders, DefaultHeaders), + FullHeaders = case OnResponse of + already_called -> Headers; + _ -> response_merge_headers(Headers, RespHeaders, DefaultHeaders) + end, Req2 = case OnResponse of + already_called -> Req; undefined -> Req; OnResponse -> OnResponse(Status, FullHeaders, Body, %% Don't call 'onresponse' from the hook itself. Req#http_req{resp_headers=[], resp_body= <<>>, - onresponse=undefined}) + onresponse=already_called}) end, ReplyType = case Req2#http_req.resp_state of waiting -> diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index 963b2f7..f5bc22d 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -846,12 +846,6 @@ generate_etag(Req, State=#state{etag=undefined}) -> case call(Req, State, generate_etag) of no_call -> {undefined, Req, State#state{etag=no_call}}; - %% Previously the return value from the generate_etag/2 callback was set - %% as the value of the ETag header in the response. Therefore the only - %% valid return type was `binary()'. If a handler returns a `binary()' - %% it must be mapped to the expected type or it'll always fail to - %% compare equal to any entity tags present in the request headers. - %% @todo Remove support for binary return values after 0.6. {Etag, Req2, HandlerState} when is_binary(Etag) -> [Etag2] = cowboy_http:entity_tag_match(Etag), {Etag2, Req2, State#state{handler_state=HandlerState, etag=Etag2}}; diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl index 4553aef..1b2ad3b 100644 --- a/src/cowboy_websocket.erl +++ b/src/cowboy_websocket.erl @@ -24,9 +24,12 @@ %% Internal. -export([handler_loop/4]). +-type close_code() :: 1000..4999. +-export_type([close_code/0]). + -type frame() :: close | ping | pong | {text | binary | close | ping | pong, binary()} - | {close, 1000..4999, binary()}. + | {close, close_code(), binary()}. -export_type([frame/0]). -type opcode() :: 0 | 1 | 2 | 8 | 9 | 10. @@ -645,7 +648,8 @@ websocket_send_many([Frame|Tail], State) -> Error -> Error end. --spec websocket_close(#state{}, Req, any(), {atom(), atom()}) +-spec websocket_close(#state{}, Req, any(), + {atom(), atom()} | {remote, close_code(), binary()}) -> {ok, Req, cowboy_middleware:env()} when Req::cowboy_req:req(). websocket_close(State=#state{socket=Socket, transport=Transport}, diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 3200188..4791f36 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -45,6 +45,7 @@ -export([nc_zero/1]). -export([onrequest/1]). -export([onrequest_reply/1]). +-export([onresponse_capitalize/1]). -export([onresponse_crash/1]). -export([onresponse_reply/1]). -export([pipeline/1]). @@ -84,7 +85,8 @@ all() -> {group, http_compress}, {group, https_compress}, {group, onrequest}, - {group, onresponse} + {group, onresponse}, + {group, onresponse_capitalize} ]. groups() -> @@ -146,6 +148,9 @@ groups() -> {onresponse, [], [ onresponse_crash, onresponse_reply + ]}, + {onresponse_capitalize, [], [ + onresponse_capitalize ]} ]. @@ -250,6 +255,18 @@ init_per_group(onresponse, Config) -> ]), {ok, Client} = cowboy_client:init([]), [{scheme, <<"http">>}, {port, Port}, {opts, []}, + {transport, Transport}, {client, Client}|Config]; +init_per_group(onresponse_capitalize, Config) -> + Port = 33086, + Transport = ranch_tcp, + {ok, _} = cowboy:start_http(onresponse_capitalize, 100, [{port, Port}], [ + {env, [{dispatch, init_dispatch(Config)}]}, + {max_keepalive, 50}, + {onresponse, fun onresponse_capitalize_hook/4}, + {timeout, 500} + ]), + {ok, Client} = cowboy_client:init([]), + [{scheme, <<"http">>}, {port, Port}, {opts, []}, {transport, Transport}, {client, Client}|Config]. end_per_group(Group, Config) when Group =:= https; Group =:= https_compress -> @@ -569,7 +586,8 @@ keepalive_max(Config) -> URL = build_url("/", Config), ok = keepalive_max_loop(Client, URL, 50). -keepalive_max_loop(_, _, 0) -> +keepalive_max_loop(Client, _, 0) -> + {error, closed} = cowboy_client:response(Client), ok; keepalive_max_loop(Client, URL, N) -> Headers = [{<<"connection">>, <<"keep-alive">>}], @@ -588,7 +606,8 @@ keepalive_nl(Config) -> URL = build_url("/", Config), ok = keepalive_nl_loop(Client, URL, 10). -keepalive_nl_loop(_, _, 0) -> +keepalive_nl_loop(Client, _, 0) -> + {error, closed} = cowboy_client:response(Client), ok; keepalive_nl_loop(Client, URL, N) -> Headers = [{<<"connection">>, <<"keep-alive">>}], @@ -671,6 +690,21 @@ onrequest_hook(Req) -> Req3 end. +onresponse_capitalize(Config) -> + Client = ?config(client, Config), + {ok, Client2} = cowboy_client:request(<<"GET">>, + build_url("/", Config), Client), + {ok, Transport, Socket} = cowboy_client:transport(Client2), + {ok, Data} = Transport:recv(Socket, 0, 1000), + false = nomatch =:= binary:match(Data, <<"Content-Length">>). + +%% Hook for the above onresponse_capitalize test. +onresponse_capitalize_hook(Status, Headers, Body, Req) -> + Headers2 = [{cowboy_bstr:capitalize_token(N), V} + || {N, V} <- Headers], + {ok, Req2} = cowboy_req:reply(Status, Headers2, Body, Req), + Req2. + onresponse_crash(Config) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, diff --git a/test/http_handler_echo_body.erl b/test/http_handler_echo_body.erl index e4b1ee0..37c2072 100644 --- a/test/http_handler_echo_body.erl +++ b/test/http_handler_echo_body.erl @@ -8,8 +8,8 @@ init({_, http}, Req, _) -> {ok, Req, undefined}. handle(Req, State) -> - {true, Req1} = cowboy_req:has_body(Req), - {ok, Body, Req2} = cowboy_req:body(Req1), + true = cowboy_req:has_body(Req), + {ok, Body, Req2} = cowboy_req:body(Req), {Size, Req3} = cowboy_req:body_length(Req2), Size = byte_size(Body), {ok, Req4} = cowboy_req:reply(200, [], Body, Req3), |