Skip to main content

Form Data and Multipart

Two flavors of form posting. Pick based on what you're shipping:

  • 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's just key1=val1&key2=val2, with values percent-encoded. Content-Type is application/x-www-form-urlencoded.

Go gives you net/url.Values to build the body, then you ship 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 client has client.PostForm(ctx, url, bodyBytes) too, which sets the Content-Type for you.

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 "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. If you're shipping more than a kilobyte of text, multipart is usually the better fit. Megabytes? You almost certainly want multipart for file fields, or just JSON.

Multipart

Multipart wraps each field in its own MIME part with a boundary string. You get a Content-Type like multipart/form-data; boundary=----WebKitFormBoundaryAbCdEf123. 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--

You almost never want to write this by hand. Use the helpers, that's what they're for.

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 really 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. Your RAM will thank you.

Order of fields

Multipart preserves the order you list fields. For most servers that doesn't matter (they parse into a map). But occasionally:

  • 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.

Seeing weird "field missing" or "invalid form" errors when the values look fine? Try reordering. Drop the file last, drop the token first. The browser-emitted order is whatever the <form> markup says, which is 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, the boundary is just a delimiter. If you hit a strict WAF that pattern-matches on browser-style boundaries, you can build the body yourself with mime/multipart.Writer.SetBoundary() and pass a Chrome-shaped one. Rare scenario though.

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)

If files is empty but you sent a file, your boundary or part headers are busted. If the file content looks like garbage, your encoding's off.