Compendium of web patterns

Issues

  1. Rework the layout of the page: instead sections with procedure signature, use one (or maybe two levels) with plain old natural language small description;

  2. hyper server does not aim to support bigger than memory request or response bodies; one way to work around it is to propose a way to interop with a low-level http reader and writer library;

  3. Rework hyper-spawn argument called proc to take two arguments: app, and request; and describe procedure to access method, path, params, headers, and body;

hyper

(hyper-spawn ip port init proc)

Start listening for HTTP request on Internet address represented as a string IP, and port number PORT. INIT is called only once, and its result is passed as first argument of every call to PROC.

For each incoming request, execute PROC with the following arguments:

  1. app: the return value of INIT;
  2. method: a symbol built from the downcased HTTP method;
  3. path: a list of strings that build the path of the HTTP request line;
  4. params: an association list where keys are symbols and values are lists of strings that is the result of parsing the "query string" part of the HTTP request line's path;
  5. headers: an association list where keys are symbols and values are strings, representing the HTTP headers of the request;
  6. body: a bytevector that represent the body of the HTTP request, it can be a bytevector of length zero;

PROC is expected to return three values:

  1. An positive integer will be the HTTP code of the response;
  2. An association list where keys are symbols, and values are strings, that will be the HTTP headers of the response;
  3. A bytevector that will be the body of the response;

(hyper-html5-write sxml)

Returns a bytevector representing SXML encoded in UTF-8 HTML5. Raises an object that satisfy hyper-html5-error? when sxml can not be encoded into JSON.

(hyper-json-read bytevector)

Returns a Scheme object according to the specification SRFI-180 of the JSON object encoded in BYTEVECTOR. Raises an object that satisfy hyper-json-error? when bytevector can not be read as a Scheme object.

(hyper-json-write obj)

Returns a bytevector encoding OBJ into a JSON object according to the specification of SRFI-180. Raises an object that satisfy hyper-json-error? when OBJ can not be encoded into JSON.

Example

(define my-app
  (lambda (_ method path params headers body)
    (match (cons method path)
      (('GET "n" "compendium-of-web-patterns")
       (values 200
               '((content-type . "text/html"))
               (hyper-html-write '(html (body "Azul dunith!"))))))))

(hyper "192.168.0.1" 1337 list my-app)

Notes: patterns, and anti-patterns

  1. Using exceptions to validate input is an anti-pattern: it is unlikely that 1) you call shallow, and deep validation 2) forget about it, and proceed to transfer 42 millions euros without checking that it is my bank account. A more relatable example: say you have a route to handle username allocation, 1) you make two shallow checks: a) the username contains only alphanumeric ascii chars b) the username is at least 3 chars long, and at most 30 chars long; 2) you check the username is not already used; if you did both 1 and 2, and at least one of the check fail, why will you nonetheless create a user with that username? That would be a bug, that is an error, but not an exceptional condition since you know that it can happen.

    Calling raise slow down the program, and you can avoid it without workarounds.

    A good reason to use raise is when a condition or various relatable conditions may happen in several procedure calls deep in the callstack, and the way to handle that behavior in a way that is clean, proper is to implement the handling guard somewhere a few procedure calls above where it happens. Example: imagine you have to make remote procedure calls over the network against one or more other programs; all those calls are related, they all succceed or the whole operation will be considered a failure; because it is a complex workflow, the actual network calls are two or more level deeper in the stack, passing around the error to push back up the error state is cluttering the code, and you have to do that with calls that may me nested at different levels: imagine several nested if, cond, case, match. A precise case of that is the communication with a remote SQL database, every call to the database whether it is a SELECT or INSERT or DELETE or whatnot will try to call the database over the network, all of those calls can fail because the network link can break; imagine for example you check that an username is available, it is, then the network link is broken, when the network is back, and to preserve consistency you need to restart the transaction from scratch ie. check again the username is available, then allocate that username. That would look something like:

    (define (maybe-allocate-username username client)
      (if (username-available? username)
          (allocate-username username client)
          #f))
    

    Both username-available? and allocate-username can fail because of a network failure. Try to imagine the number of if to include to check for network failures, and propagate that in a way that can be processed up the stack in a meaningful way, that is: another way to express the above code. Both username-available? and allocate-username may raise an object that explains there is a network failure, while the purpose of the procedure is very understandable.

    Only use raise, if an error can happen further down the stack. Otherwise, if it is doable to locally handle errors such as input validation, or domain specific errors, do it locally with return values, or an accumulator.

  2. Systematically wrapping a whole request-response with a transaction begin, then commit, otherwise rollback; prefer to use of ad-hoc and explicit (call-with-transaction database proc) inside the path handler.

    What this anti-pattern could look like using the hyper server is something like:

    (define (some-boilerplate-ish-framework ip port init proc)
      (hyper ip port init
             (lambda (app method path params headers body)
               (call-with-transaction (app-database app)
                 (lambda (tx)
                   (let ((app/tx (make-app/tx app tx)))
                     (proc app/tx method path params headers body)))))))
    

    The above will systematically start a transaction called tx, and commit it inside call-with-transaction when there is nothing that is raised that bubbles up. One might say, let's bubble-up errors, and avoid to commit dubious data; that does not work in the general case... why?

  3. Using only auto-commit is an anti-pattern.

  4. Form rendering, and form validation are orthogonal concerns.

  5. Using strings as intermediate representation of HTML5 fragmentsh.