Skip to main content

Form Data and Multipart

Two flavors of form posting. Pick based on the payload:

  • application/x-www-form-urlencoded for plain key/value text fields. Small, simple, no binary.
  • multipart/form-data for file uploads, mixed text + file fields, or anything binary.

Browsers pick the same way: <form> with enctype="multipart/form-data" for uploads, default url-encoded otherwise.

URL-encoded

The body is key1=val1&key2=val2 with values percent-encoded, and the Content-Type is application/x-www-form-urlencoded.

Go gives you net/url.Values to build the body, then ships the bytes.

package main

import (
"bytes"
"context"
"fmt"
"net/url"

httpcloak "github.com/sardanioss/httpcloak"
)

func main() {
s := httpcloak.NewSession("chrome-latest")
defer s.Close()

form := url.Values{}
form.Set("user", "alice")
form.Set("token", "abc 123")

req := &httpcloak.Request{
Method: "POST",
URL: "https://httpbin.org/post",
Headers: map[string][]string{
"Content-Type": {"application/x-www-form-urlencoded"},
},
Body: bytes.NewReader([]byte(form.Encode())),
}
resp, _ := s.Do(context.Background(), req)
defer resp.Close()

body, _ := resp.Text()
fmt.Println(body) // "form": {"user": "alice", "token": "abc 123"}
}

The non-session API has httpcloak.Client.PostForm(ctx, url, formData) where formData is a url.Values (the stdlib type from net/url), not a []byte. The wrapper handles encoding and sets the Content-Type. Build the client with httpcloak.New(preset, opts...).

Things to keep in mind:

  • Encoding is fixed. Always UTF-8 percent-encoded. Don't try Latin-1 in form bodies even if a server claims it would accept it.
  • Repeated keys. url-encoded supports the same key twice (a=1&a=2). Most builders give you a list-valued helper. In Go: form.Add("a", "1"); form.Add("a", "2").
  • Length limits. Some servers cap form bodies around 1MB. For more than a kilobyte of text, multipart is usually the better fit. For megabytes, multipart with file fields is the right pick, or just JSON.

Multipart

Multipart wraps each field in its own MIME part with a boundary string. The Content-Type looks like multipart/form-data; boundary=----WebKitFormBoundaryAbCdEf123, and the body looks roughly like:

------WebKitFormBoundaryAbCdEf123
Content-Disposition: form-data; name="comment"

hello there
------WebKitFormBoundaryAbCdEf123
Content-Disposition: form-data; name="file"; filename="note.txt"
Content-Type: text/plain

file body bytes
------WebKitFormBoundaryAbCdEf123--

Writing this by hand is almost never worth it. The helpers exist for exactly this.

httpcloak ships MultipartField and BuildMultipart for the common case.

package main

import (
"bytes"
"context"
"fmt"

httpcloak "github.com/sardanioss/httpcloak"
)

func main() {
s := httpcloak.NewSession("chrome-latest")
defer s.Close()

fields := []httpcloak.MultipartField{
{Name: "comment", Value: "hello there"},
{
Name: "file",
Filename: "note.txt",
Content: []byte("file body bytes"),
ContentType: "text/plain",
},
}
body, contentType, err := httpcloak.BuildMultipart(fields)
if err != nil {
panic(err)
}

req := &httpcloak.Request{
Method: "POST",
URL: "https://httpbin.org/post",
Headers: map[string][]string{
"Content-Type": {contentType},
},
Body: bytes.NewReader(body),
}
resp, _ := s.Do(context.Background(), req)
defer resp.Close()

body2, _ := resp.Text()
fmt.Println(body2)
// form: { "comment": "hello there" }
// files: { "file": "file body bytes" }
}

BuildMultipart handles boundary generation, MIME headers per part, and the trailing --. The Content-Type it returns is the full string with the boundary parameter, drop it straight into your headers.

For large file uploads, building the whole body in memory is a bad idea. Use mime/multipart.Writer directly with io.Pipe and stream into the request via Body: pipeReader.

Order of fields

Multipart preserves the order you list fields. Most servers parse into a map and don't care, but a few do:

  • A server rejects an upload if the file part comes before a CSRF token field.
  • A server expects a specific field order baked into its form validation.

When values look fine but you're seeing "field missing" or "invalid form" errors, try reordering. Drop the file last, the token first. The browser-emitted order follows whatever the <form> markup says, usually text fields then file inputs.

Boundary string

Browsers use boundaries like ----WebKitFormBoundaryAbCdEf123456. Go's stdlib (and therefore httpcloak's BuildMultipart) generates random boundaries that look different. For 99% of servers this doesn't matter since the boundary is just a delimiter. A strict WAF that pattern-matches on browser-style boundaries can be handled by building the body yourself with mime/multipart.Writer.SetBoundary() and passing a Chrome-shaped one. Rare scenario.

Verify with httpbin

POST https://httpbin.org/post is the one-stop shop for verifying multipart serialization. The response gives you:

  • form: text fields
  • files: file fields, with the file content inlined as a string
  • headers["Content-Type"]: the Content-Type you sent (verify the boundary)

An empty files when you sent a file means your boundary or part headers are off. Garbage-looking file content means the encoding is wrong.