File uploads & binary bodies

Upload a file with multipart/form-data or send raw bytes from a request node, including inline base64 and the content types Bandura sets for you.

5 min read

A request node’s body is a plain string most of the time: JSON, XML, form-encoded text, whatever you typed. Two things cannot be expressed that way, so body also accepts a structured block picked by its type field.

Uploading a file

- id: upload_avatar
  type: request
  config:
    method: POST
    url: ${{ variables.baseUrl }}/users/{id}/avatar
    pathParams:
      id: ${{ variables.userId }}
    body:
      type: multipart
      fields:
        - name: caption
          value: Profile photo # a plain text field
        - name: file
          filePath: ./fixtures/avatar.png # resolved relative to the flow file
          contentType: image/png # optional, defaults to application/octet-stream
          filename: avatar.png # optional, defaults to the basename of filePath
  next: verify_upload

Each entry in fields is either a text field (value) or a file read from disk (filePath). Exactly one of the two: setting both, or neither, is a parse error rather than a surprise at run time.

Bandura writes the Content-Type header itself, including the generated boundary, and it overrides any Content-Type your node’s headers set. A hand-written boundary could never match the one the body was actually built with, so there is nothing useful to preserve.

Uploading something you just generated

A text field may also carry a filename. That sends its inline value as a named file part rather than an ordinary form field, which is how content a script node produced gets uploaded without first writing it to disk:

Needs @bandura/cli 1.0.0-rc.4 or the desktop app 1.0.0-rc.5 or newer; every current build qualifies. The parser has always accepted filename on a text field, but desktop 1.0.0-rc.4 and earlier drop it and ship the part as a plain form field, so the server sees a different request with nothing to say so.

- id: build_csv
  type: script
  config:
    code: |
      variables.report = "id,total\n1,9.99\n2,14.50\n";
  next: upload_csv

- id: upload_csv
  type: request
  config:
    method: POST
    url: ${{ variables.baseUrl }}/imports
    body:
      type: multipart
      fields:
        - name: file
          value: ${{ variables.report }}
          filename: report.csv
          contentType: text/csv
  next: null

Sending raw bytes

- id: upload_report
  type: request
  config:
    method: POST
    url: ${{ variables.baseUrl }}/reports
    body:
      type: binary
      filePath: ./fixtures/report.pdf
      contentType: application/pdf
  next: null

type: binary sends the bytes as the whole body, with no encoding around them. Use it for the APIs that take a file directly on PUT or POST rather than wrapped in a form.

The source is either filePath or an inline base64 string, again exactly one of the two:

body:
  type: binary
  base64: ${{ $base64(variables.generatedPayload) }}
  contentType: application/octet-stream

contentType is required here, because there is no sensible default for arbitrary bytes. It becomes the Content-Type header only if your headers did not already set one, in any casing.

Where file paths resolve

filePath is resolved relative to the flow file that declares it, unless it is already absolute. That is the same convention tls.cert and a gRPC node’s proto use, so a flow and its fixtures move together as one folder.

A path that does not resolve to a readable file fails the node with an error naming the field, the path it tried, and the directory it tried it from. You will not get a bare ENOENT with no indication of which of five fields caused it.

Interpolation works throughout

Every string in either block goes through ${{ }}: a field’s name, value, filePath, contentType and filename, and a binary body’s filePath, base64 and contentType. So the fixture directory can come from an environment variable, and dynamic values work here as they do anywhere else.

Editing one in the app

Everything above is editable from the inspector’s Body tab, which follows the shape of the body rather than assuming it is a string.

For a multipart body you get one card per field, each switching between a text value and a file. Choose file… opens your operating system’s picker. filename and contentType sit on the field that has them, which makes filename reachable outside the YAML for the first time. For a binary body you pick between a filePath and an inline base64 string and set the contentType the format requires.

Two things about the editors are worth knowing, because both are deliberate:

  • The picker writes an absolute path. It has no way to know which flow is open, and an absolute path is always correct. Shortening it to a relative one is a manual edit, and the panel says what a relative path resolves against rather than guessing a base for you.
  • An edit is held back until it validates. Adding a field, or flipping one from text to file, passes through a state the format does not accept. Rather than write that and have the flow go stale, the panel shows what you are typing while the file keeps the last version that parsed, and tells you it is doing so. The moment the block is valid again it is written.

The panel also names what the body is, listing a multipart body’s field names or a binary body’s source and content type, and repeats for a multipart body that the engine owns the Content-Type header.

On 1.0.0-rc.7 and earlier there are no structured editors, and the Body tab points you at the Node tab’s YAML instead. The summary above needs rc.5 or newer: on rc.4 and earlier the Body tab and the request preview render a node with a structured body as though it sends no body at all. What went on the wire was never affected, only the panels describing it.

Things worth knowing

  • A structured body and graphql are mutually exclusive, the same rule a string body already had. A GraphQL request builds its own body from graphql.query.
  • The Result panel shows what actually shipped. For a structured body, the request snapshot in the inspector and in the run history is the built wire bytes, not the YAML block you wrote. That is the same “what got sent” contract a hand-written body gives.
  • Large files are read into memory for the request. Bandura is a testing tool, so this is fine for fixtures and unhelpful for a multi-gigabyte upload.

Last updated

Looking for something else? All 37 articles are on one page in the Help Center.