Fuzion Logo
fuzion-lang.dev — The Fuzion Language Portal
JavaScript seems to be disabled. Functionality is limited.

http/Message.fz


# This file is part of the Fuzion language implementation.
#
# The Fuzion language implementation is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, version 3 of the License.
#
# The Fuzion language implementation is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU General Public License along with The
# Fuzion language implementation.  If not, see <https://www.gnu.org/licenses/>.


# -----------------------------------------------------------------------
#
#  Tokiwa Software GmbH, Germany
#
#  Source code of Fuzion http library feature Message
#
# -----------------------------------------------------------------------


# HTTP message, can be a request or a response
#
public Message ref is



  module version_str(major, minor i32) => "HTTP/" + (major > 1 && minor = 0 ? $major : "$major.$minor")


  # the start line of the message, for requests it is the request line, for responses the status line
  #
  public start_line String => abstract


  # header fields with all lower case
  #
  public header container.Map String String => abstract


  # body of the message, if it has one
  #
  # NYI: CLEANUP: should probably be possible for body to be
  # array u8, nil or a specify an LM to read from.
  # The question is what do we need?
  #
  public body io.Read_Handler => abstract


  # get the body of the message as a String
  #
  public body_as_string String =>
    lm : mutate is
    lm ! ()->
      io
        .buffered lm
        .reader body ! ()->
          _ : String is
            public redef utf8 Sequence u8 := (io.buffered lm).read_fully


  # the whole message as a sequence of bytes
  #
  public bytes(
    # the maximum number of bytes to read from the body,
    # does not account for the size of the header
    max_body_bytes i32) Sequence u8
  =>
    # NYI: BUG: read fully
    b := match body.read max_body_bytes
      s Sequence => s
      * => []
    (start_line + header_as_canonical_string + crlf).utf8 ++ b


  # String representation of the message header
  #
  public redef as_string String => start_line + header_as_canonical_string + crlf


  # String representation of the whole message, i.e. header and body
  #
  public as_string_with_body(
    # the maximum number of bytes to read from the body,
    # does not account for the size of the header
    max_body_bytes i32) outcome String
  =>
    match body.read max_body_bytes
      s Sequence => $Message.this + String.from s
      e error => e
      io.end_of_file => $Message.this


  # NYI: PERFORMANCE: see validHeaderFieldByte() in https://go.dev/src/net/textproto/reader.go
  valid_header_field_byte(c codepoint) bool =>
    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#\$%&'*+-.^_|~".contains c


  # e.g.:
  #
  # accept-language -> Accept-Language
  #
  canonical_header_key(str String) String =>
    if str.codepoints.drop_while valid_header_field_byte .is_empty
      str
        .split "-"
        .map s->
          (s.substring 0 1 .upper_case) + s.substring 1 .lower_case
        .as_string "-"
    else
      str


  # the header as a canonical string (with normalized keys).
  #
  # e.g.:
  #
  # Accept-Encoding: gzip, deflate, br
  # Accept-Language: en-GB,en;q=0.5
  # Connection: keep-alive
  # Host: www.example.com
  # User-Agent: Mozilla/5.0
  #
  # NYI: PERFORMANCE: compute this only once
  #
  header_as_canonical_string =>
    header
      .items
      .map kv->
        k, v := kv
        "{canonical_header_key k}: $v" + crlf
      .as_string ""



# Although the line terminator for the start-line and fields is the sequence CRLF,
# a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR.
# https://datatracker.ietf.org/doc/html/rfc9112#section-2.2-3
#
module next_line(LM type : mutate) => (io.buffered LM).read_delimiter "\n".utf8[0] true



module parse_header_fields(LM type : mutate) outcome (container.mutable_tree_map LM String String) =>

  exception unit _ ()->

    cause(s String) void =>
      (exception unit).env.cause (error s)

    fields := (container.mutable_tree_map LM String String) .empty

    _ :=
      for cur := match next_line LM
                    io.end_of_file => cause "unexpected end of input"
                    s String => s
      while cur != ""
      do

        # A recipient that receives whitespace between the start-line and the first header field MUST either reject the message as invalid or consume each whitespace-preceded line without further processing of it
        # https://datatracker.ietf.org/doc/html/rfc9112#section-2.2-8
        if cur.starts_with " " then cause "whitespace between start line and header field"

        parts := cur.split_n ":" 1
        if parts.count != 2 then cause "broken header field '$cur'"
        # NYI: UNDER DEVELOPMENT: must duplicate header fields cause an error?
        fields.put parts[0].lower_case parts[1].trim

    fields


module body_helper(LM type : mutate, is_req bool, header_fields container.Map String String) outcome io.Read_Handler =>
  exception unit io.Read_Handler ()->

    cause(s String) void =>
      (exception unit).env.cause (error s)

    # If a message is received with both a Transfer-Encoding and a Content-Length header field, the Transfer-Encoding overrides the Content-Length.
    # Such a message might indicate an attempt to perform request smuggling (Section 11.2) or response splitting (Section 11.1) and ought to be handled as an error.
    # An intermediary that chooses to forward the message MUST first remove the received Content-Length field and process the Transfer-Encoding (as described below) prior to forwarding the message downstream.
    # https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.3
    if header_fields.has "transfer-encoding" && header_fields.has "content-length"
      cause "request header contains both 'transfer-encoding' and 'content-length' field"

    else if header_fields.has "transfer-encoding"

      # transfer codings are case-insensitive
      # https://www.rfc-editor.org/rfc/rfc7230#section-4
      transfer_encodings :=
        header_fields["transfer-encoding"]
          .or_panic
          .split ","
          .map (.trim.lower_case)

      # If a Transfer-Encoding header field is present and the chunked transfer coding (Section 7.1) is the final encoding, the message body length is determined by reading and decoding the chunked data until the transfer coding indicates the data is complete.
      if transfer_encodings = [ "chunked" ]
        body_reader_chunked LM
      else
        cause "message with unsupported transfer encoding"

    else if header_fields.has "content-length"
      match header_fields["content-length"].or_panic.parse_u64
        error =>

          # NYI: UNDER DEVELOPMENT: implement recovery
          # unless the field value can be successfully parsed as a comma-separated list (Section 5.6.1 of [HTTP]), all values in the list are valid, and all values in the list are the same (in which case, the message is processed with that single value used as the Content-Length field value)

          cause "invalid value for field content-length"

        length u64 =>
          # content Sequence u8 := (io.buffered LM).read_bytes length
          # (content.count = length) ? option content : (cause "input closed after {content.count} bytes, but specified content-length: $length")
          body_reader LM length

    # If this is a request message and none of the above are true, then the message body length is zero (no message body is present).
    # Otherwise, this is a response message without a declared message body length, so the message body length is determined by the number of octets received prior to the server closing the connection.
    # https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.7
    else
      # is_req ? nil : option (Sequence u8) (io.buffered LM).read_fully
      body_reader LM (is_req ? 0 : nil)



/*
NYI:

Reconstructing the Target URI
https://datatracker.ietf.org/doc/html/rfc9112#name-reconstructing-the-target-u

Obsolete Line Folding
https://datatracker.ietf.org/doc/html/rfc9112#name-obsolete-line-folding

Missing, everything from 8 and up
https://datatracker.ietf.org/doc/html/rfc9112#name-handling-incomplete-message

HTTP does not place a predefined limit on the length of a request-line, as described in Section 2.3 of [HTTP]. A server that receives a method longer than any that it implements SHOULD respond with a 501 (Not Implemented) status code. A server that receives a request-target longer than any URI it wishes to parse MUST respond with a 414 (URI Too Long) status code (see Section 15.5.15 of [HTTP]). Various ad hoc limitations on request-line length are found in practice. It is RECOMMENDED that all HTTP senders and recipients support, at a minimum, request-line lengths of 8000 octets.
https://datatracker.ietf.org/doc/html/rfc9112#section-3-4

A client MUST send a Host header field (Section 7.2 of [HTTP]) in all HTTP/1.1 request messages. If the target URI includes an authority component, then a client MUST send a field value for Host that is identical to that authority component, excluding any userinfo subcomponent and its "@" delimiter (Section 4.2 of [HTTP]). If the authority component is missing or undefined for the target URI, then a client MUST send a Host header field with an empty field value.
https://datatracker.ietf.org/doc/html/rfc9112#section-3.2-5


*/

last changed: 2026-06-12