diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6a8f7..4d11cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,10 @@ # Unreleased -- Remove all usage of `string.inspect` as it's a huge anti-pattern for logging. -- Patch infinite loops and adjust allowed entries for trailer headers. +- Remove all usage of `string.inspect` as it is an anti-pattern for logging. +- Fix infinite loop and adjust allowed entries for trailer headers. +- Improve and expand logging messages. +- Improve path parsing. # v3.0.4 - 13.03.2026 diff --git a/src/ewe/internal/http1.gleam b/src/ewe/internal/http1.gleam index c346569..fd0016c 100644 --- a/src/ewe/internal/http1.gleam +++ b/src/ewe/internal/http1.gleam @@ -23,7 +23,6 @@ import gleam/result.{replace_error, try} import gleam/set.{type Set} import gleam/string import gleam/string_tree.{type StringTree} -import gleam/uri import glisten import glisten/socket.{type Socket} import glisten/transport.{type Transport} @@ -90,7 +89,7 @@ fn read_from_socket( pub type ParseError { // request line InvalidMethod - InvalidTarget + InvalidPath InvalidVersion // headers InvalidHeaders @@ -143,10 +142,10 @@ pub fn parse_request( |> replace_error(InvalidMethod), ) - use uri <- try( + use #(path, query) <- try( bit_array.to_string(target) - |> try(uri.parse) - |> replace_error(InvalidTarget), + |> try(parse_path) + |> replace_error(InvalidPath), ) // Headers @@ -190,8 +189,8 @@ pub fn parse_request( scheme:, host:, port:, - path: uri.path, - query: uri.query, + path:, + query:, ) case version { @@ -233,6 +232,9 @@ pub fn parse_request( } } +@external(erlang, "ewe_ffi", "parse_path") +fn parse_path(string: String) -> Result(#(String, option.Option(String)), Nil) + /// Parses HTTP headers from the buffer. /// fn parse_headers( diff --git a/src/ewe/internal/http1/handler.gleam b/src/ewe/internal/http1/handler.gleam index 9658625..36187e2 100644 --- a/src/ewe/internal/http1/handler.gleam +++ b/src/ewe/internal/http1/handler.gleam @@ -96,11 +96,55 @@ pub fn handle_packet( } } Error(reason) -> { - let status = case reason { - ewe_http.InvalidVersion -> 505 - _ -> 400 + let #(status, message) = case reason { + ewe_http.InvalidMethod -> #( + 400, + "Rejected HTTP request with invalid method", + ) + ewe_http.InvalidPath -> #( + 400, + "Rejected HTTP request with invalid path", + ) + ewe_http.InvalidVersion -> #( + 505, + "Rejected HTTP request with unsupported version", + ) + ewe_http.InvalidHeaders -> #( + 400, + "Rejected HTTP request with malformed headers", + ) + ewe_http.MissingHost -> #( + 400, + "Rejected HTTP request with missing Host header", + ) + ewe_http.DuplicateHost -> #( + 400, + "Rejected HTTP request with duplicate Host header", + ) + ewe_http.InvalidContentLength -> #( + 400, + "Rejected HTTP request with invalid Content-Length", + ) + ewe_http.InvalidBody -> #( + 400, + "Rejected HTTP request with malformed body", + ) + ewe_http.BodyTooLarge -> #( + 400, + "Rejected HTTP request with body exceeding size limit", + ) + ewe_http.MalformedRequest -> #( + 400, + "Rejected HTTP request due to malformed packet", + ) + ewe_http.PacketDiscard -> #( + 400, + "Rejected HTTP request due to unrecognized packet", + ) } + logging.log(logging.Warning, message) + let _ = response.new(status) |> response.set_body(<<>>) diff --git a/src/ewe/internal/stream/chunked.gleam b/src/ewe/internal/stream/chunked.gleam index c5ab1f5..8bad792 100644 --- a/src/ewe/internal/stream/chunked.gleam +++ b/src/ewe/internal/stream/chunked.gleam @@ -75,20 +75,17 @@ pub fn start( } Error(socket_reason) -> { let message = - "Socket error occured while trying to send the end of chunked response: " + "Failed to send chunked response terminator: " <> socket.reason_to_string(socket_reason) - logging.log(logging.Error, message) + logging.log(logging.Warning, message) on_close(conn, state) actor.stop_abnormal(message) } } } AbnormalStop(reason) -> { - logging.log( - logging.Error, - "Chunked response stopped abnormally: " <> reason, - ) + logging.log(logging.Warning, "Chunked response stopped: " <> reason) on_close(conn, state) actor.stop_abnormal(reason) } diff --git a/src/ewe/internal/stream/websocket.gleam b/src/ewe/internal/stream/websocket.gleam index 736494c..72a4c81 100644 --- a/src/ewe/internal/stream/websocket.gleam +++ b/src/ewe/internal/stream/websocket.gleam @@ -366,10 +366,11 @@ fn handle_close( case abnormal_reason { Some(reason) -> { - logging.log( - logging.Error, - "WebSocket connection closed abnormally: " <> reason, - ) + let level = case reason == crashed { + True -> logging.Error + False -> logging.Warning + } + logging.log(level, "WebSocket closed: " <> reason) actor.stop_abnormal(reason) } None -> actor.stop() diff --git a/src/ewe_ffi.erl b/src/ewe_ffi.erl index cc1c653..5dd0aa5 100644 --- a/src/ewe_ffi.erl +++ b/src/ewe_ffi.erl @@ -1,13 +1,16 @@ -module(ewe_ffi). -export([close_file/1, decode_packet/3, init_clock_storage/0, lookup_http_date/0, now/0, - now_microseconds/0, open_file/1, set_http_date/1, validate_field_value/1, coerce_tcp_message/1]). + now_microseconds/0, open_file/1, set_http_date/1, validate_field_value/1, + coerce_tcp_message/1, parse_path/1]). % Socket % ----------------------------------------------------------------------------- -coerce_tcp_message({tcp, _Socket, Data}) -> Data; -coerce_tcp_message({ssl, _Socket, Data}) -> Data. +coerce_tcp_message({tcp, _Socket, Data}) -> + Data; +coerce_tcp_message({ssl, _Socket, Data}) -> + Data. % HTTP % ----------------------------------------------------------------------------- @@ -30,6 +33,21 @@ decode_packet(Type, Packet, Options) -> {error, Reason} end. +parse_path(Value) -> + case uri_string:parse(Value) of + {error, _, _} -> + {error, nil}; + Uri -> + Query = + try + {some, maps:get(query, Uri)} + catch + _:_ -> + none + end, + {ok, {maps:get(path, Uri), Query}} + end. + validate_field_value(Value) -> case do_validate_field_value(Value) of true ->