Preface

Almost all parameters of the rules file support templating. Templating means, macros are replaced by values. Templating happens twice: Before and after the rule action is performed. This allows you to create interactive actions and to customize the response. Look at this very simple example.

---
rules:
  - name: Say something
    on:
      path: /say
    answer.content: |
        Hi {{ .Input.Params.name }}.
        You said: "{{ .Input.Form.text }}"        

To see it in action fire a request like this:

$ curl localhost:3000/say?name=John -F text="Summer isn't warm."
Hi John.
You said: "Summer isn't warm."

Before the script is handed over the interpreter all macros inside the curly braces are processed and - if possible - replaced by its values.

Request-Response flow

sequenceDiagram
   participant Req as Client
   participant Reqh as Request Handler
   participant A as Action
   participant Resh as Response Handler
   
   Req->>Reqh: Request GET /say?name=John
   rect rgb(191, 223, 255)
     Reqh->>Reqh: Collect Meta and Input data as Request Data
   end
   Reqh->>A: Request data
   rect rgb(191, 223, 255)
       A->>A: Render action with request data
       A->>A: Perform action
   end
   A->>Resh: Request Data
   A->>Resh: Action Data
   rect rgb(191, 223, 255)
       Resh->>Resh: Render response template with Request and Action data.
   end
   Resh->>Client: Response

Input macros

Input macros are those used in the action specified by the do object. Templating happens before the action is executed.

URL Placeholders

If the path contains one or many placeholders in single curly braces, they become available under the .Input.URLPlaceholders.<FIELD> variable. The field is case-sensitive. Addressing a nonexistent field returns an empty string.

Example:

---
rules:
  - name: URL Placeholders
    on:
      path: /say/{name}/{city}
    run.script: |
        echo "$(date) -- You are {{ .Input.URLPlaceholders.name }} from {{ .Input.URLPlaceholders.city }}"        

To see it in action, fire a request like this:

$ curl localhost:3000/say/John/London
Mon Feb 12 15:45:01 CET 2024 -- You are John from London

Form fields

Data from POST and PUT request submitted either as multipart/form-data or application/x-www-form-urlencoded are handed over to the template engine, and they are available under the .Input.Form.<FIELD> variables.

Examples:

---
rules:
  - name: Form data single field
    on:
      path: /form/single
    answer.content: Hello {{ .Input.Form.name }}

  - name: Form data iteration
    on:
      path: /form/iter
    answer.content: |
        This is your Form Input:
        {{- range $key,$value := .Input.Form }}
        * {{ $key }} = {{ $value }}
        {{- end }}        

To see it in action execute the following requests.

$ curl localhost:3000/form/single -F name=John
Hello John

$ curl localhost:3000/form/single -d name=John
Hello John

$ curl localhost:3000/form/iter -F name=John -F city=London
This is your Form Input:
* city = London
* name = John

$ curl localhost:3000/form/iter -d "name=John&city=London"
This is your Form Input:
* city = London
* name = John

JSON data

JSON data submitted with an application/json header is automatically decoded and made available to the template engine via the .Input.JSON.<FIELD> variables.

Example:

---
rules:
  - name: Json data
    on:
      path: /json
    answer.content: >-
        Hello {{ .Input.JSON.person.first_name }} 
        {{ .Input.JSON.person.last_name }}        

To see it in action fire a request as shown:

$ curl localhost:3000/json -H "content-type:application/json" \
  --data-raw '{"person":{"first_name":"John","last_name":"Doe"}}'
Hello John  Doe

Attempting to access a nonexistent field of a nested JSON which is at the top or in the middle of the hierarchy will result in an internal server error.
{{ .Input.JSON.person.last_name }} with submitted data {"name":"John"} provokes an error because person is undefined. Using Default like {{ .Input.JSON.person.last_name|Default "Doe" }} will not prevent the error because with person being undefined the error is provoked before the Default function is called.

Accessing nonexistent fields that are at the very bottom will resolve to the string <no value>.
{{ .Input.JSON.something }} with submitted data {"name":"John"} returns <no value>. {{ .Input.JSON.person.name }} with submitted data {"person":{"last_name":"Doe"} returns “” because person is defined but name not.

You can use the |Default "something" to apply a default value if the field is unknown.

File Uploads

Optionally, HTTPE accepts file uploads. They are stored in a temporary folder, and then it’s up to you to process them. Look at this example:

---
rules:
  - name: File Upload
    on:
      path: /upload
      methods:
        - post
    run.script: |
        {{ $upload := index .Input.Uploads 0 }} # Get the first upload
        echo "File upload completed: {{ $upload.FieldName }} {{ $upload.FileName }} {{ $upload.Type }}"
        echo "Stored in: {{ $upload.Stored }}"
        ls -lh {{ $upload.Stored }}        

    args:
      file_uploads: true # Default: false
    with:
      max_request_body: 5MB

To see it in action, fire the below request:

$ curl localhost:3000/upload -F my-upload=@/etc/hosts
File upload completed: my-upload hosts text/UTF-8
Stored in: /var/folders/8y/bd260j7s7n7fgkgsj_r3sjw00000gn/T//httpe_upload_CXHdAapDdZwaPnmh9yj5PE
-rw-r--r--  1 thorsten  staff   324B Feb 20 11:13 /var/folders/8y/bd260j7s7n7fgkgsj_r3sjw00000gn/T//httpe_upload_CXHdAapDdZwaPnmh9yj5PE

.Input.Uploads is an array storing information about all upload. You can access them individually using the index function as shown in the example above. Also, you can use range to iterate over all file uploads.

Accessing the .Input.Uploads with file uploads not explicitly enabled, throws an internal server error.

URL query parameters

Query parameters can be access via {{ .Input.Params.<FIELD>}}.

Example:

---
rules:
  - name: URL Query Parameters
    on:
      path: /params
    answer.content: |
        Your name is {{ .Input.Params.name }}
        You live in {{ .Input.Params.city | Default "Berlin" }}        

To see it in action fire some requests as shown:

$ curl "localhost:3000/params?name=John%20Doe&city=London"
Your name is John Doe
You live in London

$ curl localhost:3000/params?name=John%20Doe
Your name is John Doe
You live in Berlin

Output macros

You can manipulate the response of all actions by overwriting the default output template.

Example 1

---
rules:
  - name: Custom response
    on:
      path: /test
    answer.content: This is an error
    respond:
      on_success:
        body: |
          Error: {{ .Action.SuccessBody }}          
        http_status: 400

This rule will always return an error with status code 400 with the message Error: This is an error.

Let’s look at the details. An action by itself doesn’t create a response. It populates some variables with data. The response handler takes this data and renders it into a response. Actions have a default template that suits most needs. By specifying a response object in the rule, you can overwrite the default action template.
The example above defines a template that’s used if the action was successful. The answer.content action is always successful. Defining an on_error template would be meaningless.
The action stores its result in the variables .Action.SuccessBody and .Action.ErrorBody. In case of answer.content the .Action.SuccessBody equals the text to be returned.
Once the action has terminated it hands over the two mentioned variables to the repose handler. The request handler takes the template specified Error: {{ .Action.SuccessBody }} and renders the response. This results in all responses having a prefix Error: .
Additionally the default status 200 is overwritten by 400.

Example 2

---
rules:
  - name: Execute a script
    on:
        path: /script
    run.script: "{{ .Input.Form.Script }}"
    args:
      timeout: 3
    respond:
      on_error:
        body: |
          Your script '{{ .Input.Form.Script }}' has failed with: 
          Stderr: {{ .Action.ErrorBody }}
          Stdout: {{ .Action.SuccessBody }}
          Exit Code: {{ .Action.Code }}          
      on_success:
        body: |
          Your script '{{ .Input.Form.Script }}' returns:
          {{ .Action.SuccessBody }}
          {{ .Action.ErrorBody }}          

To see it in action, fire some requests.

Request 1:

cat <<EOF|curl localhost:3000/script -F Script="$(</dev/stdin)"
date
>$2 echo "Write text to stderr"
echo "Provoking an error:"
date -x || true
date
EOF

Your script 'date
> echo "Write text to stderr"
echo "Provoking an error:"
date -x || true
date' returns:
Wed Feb 21 13:29:10 CET 2024
Provoking an error:
Wed Feb 21 13:29:10 CET 2024

sh: line 2: Write text to stderr: command not found
date: illegal option -- x
usage: date [-jnRu] [-I[date|hours|minutes|seconds]] [-f input_fmt]
            [-r filename|seconds] [-v[+|-]val[y|m|w|d|H|M|S]]
            [[[[mm]dd]HH]MM[[cc]yy][.SS] | new_date] [+output_fmt]

Request 2:

$ cat <<EOF|curl localhost:3000/script -F Script="$(</dev/stdin)"
date
>$2 echo "Write text to stderr"
echo "Provoking an error:"
date -x
EOF

Your script 'date
> echo "Write text to stderr"
echo "Provoking an error:"
date -x' has failed with: 
  Stderr: sh: line 2: Write text to stderr: command not found
date: illegal option -- x
usage: date [-jnRu] [-I[date|hours|minutes|seconds]] [-f input_fmt]
            [-r filename|seconds] [-v[+|-]val[y|m|w|d|H|M|S]]
            [[[[mm]dd]HH]MM[[cc]yy][.SS] | new_date] [+output_fmt]

  Stdout: Wed Feb 21 13:28:14 CET 2024
Provoking an error:

Exit Code: 1

Request 1 uses the on_success template because the action terminates successfully. || true does the trick. It changes the exit code of any failed command to 0.

Request 2 uses the on_error template, because the date -x command fails.

Metadata Macros

Example:

---
rules:
  - name: Metadata
    on:
      path: /meta
    answer.content: |
      User Agent:  {{ .Meta.UserAgent }}
      URL:         {{ .Meta.URL }}
      Remote Add:  {{ .Meta.RemoteAddr }}
      Method:      {{ .Meta.Method }}
      X-My-Header: {{ index .Meta.Headers "X-My-Header" }}      

To see it in action, fire a request like this:

$ curl localhost:3000/meta -H "X-My-Header:this is awesome"
User Agent:  curl/8.4.0
URL:         /meta
Remote Add:  [::1]:51805
Method:      GET
X-My-Header: this is awesome

For the action and the response you can use the following macros:

  • {{ .Meta.UserAgent }}, returns the user agent as specified on the request headers.
  • {{ .Meta.URL }}, returns the URL of the request.
  • {{ .Meta.RemoteAddr }}, returns the remote address of the http client.
  • {{ .Meta.Method }}, returns the HTTP method of the request.
  • {{ .Meta.Headers.<Header>, returns any header values. If the header value is an array of values, only the first value is returned. Headers must be access always by their capitalized names. To access headers containing a hyphen, use the index function, example {{ index .Meta.Headers "X-My-Header" }}

Functions

You can pipe the output of the macro replacement through functions.

Supported functions:

  • Default "<THE-DEFAULT>, returns the default if macro returns an empty string. Example:
    {{ .Input.Params.city | Default "Berlin" }}
  • ToUpper, converts strings to uppercase
  • ToLower, converts strings to lowercase