Vix.cpp v2.6.0 is here Read the blog
Skip to content

Form Parser

This example shows how to parse application/x-www-form-urlencoded request bodies with Vix middleware.

Use this when a route receives classic HTML form data such as:

txt
name=Gaspard&email=gaspard@example.com&message=Hello
1

This format is commonly used by:

txt
HTML forms
simple contact forms
login forms
search forms
small admin forms
browser-submitted forms
1
2
3
4
5
6

For file uploads, use multipart instead.

For JSON APIs, use the JSON parser.

What this example builds

The app exposes:

txt
GET  /
POST /contact
1
2

The route /contact expects:

txt
Content-Type: application/x-www-form-urlencoded
1

and fields:

txt
name
email
message
1
2
3

The middleware parses the form and stores it in request state as:

cpp
middleware::parsers::FormBody
1

Project structure

Create:

txt
form_parser_demo/
└── form_parser.cpp
1
2

Create the file:

bash
mkdir form_parser_demo
cd form_parser_demo
touch form_parser.cpp
1
2
3

Source

Open:

txt
form_parser.cpp
1

Add:

cpp
#include <string>

#include <vix.hpp>
#include <vix/middleware.hpp>

using namespace vix;

static std::string form_value(
  const middleware::parsers::FormBody &form,
  const std::string &key)
{
  auto it = form.fields.find(key);

  if (it == form.fields.end())
    return {};

  return it->second;
}

static bool missing(const std::string &value)
{
  return value.empty();
}

static void install_middleware(App &app)
{
  app.use("/contact", middleware::app::request_id_dev());
  app.use("/contact", middleware::app::timing_dev());
  app.use("/contact", middleware::app::security_headers_dev());

  app.use("/contact", middleware::app::body_limit_write_dev(
    16 * 1024
  ));

  app.use("/contact", middleware::app::form_dev(
    4096
  ));
}

static void register_routes(App &app)
{
  app.get("/", [](Request &, Response &res)
  {
    res.send(
      "Form parser example\n"
      "\n"
      "Try:\n"
      "  curl -i -X POST http://127.0.0.1:8080/contact \\\n"
      "    -H \"Content-Type: application/x-www-form-urlencoded\" \\\n"
      "    --data \"name=Gaspard&email=gaspard@example.com&message=Hello\"\n"
    );
  });

  app.post("/contact", [](Request &req, Response &res)
  {
    auto &form =
      req.state<middleware::parsers::FormBody>();

    const std::string name = form_value(form, "name");
    const std::string email = form_value(form, "email");
    const std::string message = form_value(form, "message");

    if (missing(name))
    {
      res.status(422).json({
        "ok", false,
        "error", "Missing required field",
        "field", "name"
      });
      return;
    }

    if (missing(email))
    {
      res.status(422).json({
        "ok", false,
        "error", "Missing required field",
        "field", "email"
      });
      return;
    }

    if (missing(message))
    {
      res.status(422).json({
        "ok", false,
        "error", "Missing required field",
        "field", "message"
      });
      return;
    }

    res.json({
      "ok", true,
      "received", true,
      "name", name,
      "email", email,
      "message", message
    });
  });
}

int main()
{
  App app;

  install_middleware(app);
  register_routes(app);

  app.run(8080);
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

Run it

Run:

bash
vix run form_parser.cpp
1

The server listens on:

txt
http://127.0.0.1:8080
1

Test the home route

bash
curl -i http://127.0.0.1:8080/
1

Expected body:

txt
Form parser example
1

This route is public and does not use the form parser.

Send a valid form

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard&email=gaspard@example.com&message=Hello"
1
2
3
4

Expected status:

txt
200 OK
1

Expected body:

json
{
  "ok": true,
  "received": true,
  "name": "Gaspard",
  "email": "gaspard@example.com",
  "message": "Hello"
}
1
2
3
4
5
6
7

The form parser decodes the body and stores the fields in request state.

URL decoding

The parser handles URL-encoded values.

Example:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard+Kirira&email=gaspard%40example.com&message=Hello+from+Vix"
1
2
3
4

Expected decoded values:

txt
name
  Gaspard Kirira

email
  gaspard@example.com

message
  Hello from Vix
1
2
3
4
5
6
7
8

In URL-encoded forms:

txt
+
  becomes a space

%40
  becomes @
1
2
3
4
5

Test missing field

Missing message:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard&email=gaspard@example.com"
1
2
3
4

Expected status:

txt
422 Unprocessable Entity
1

Expected body:

json
{
  "ok": false,
  "error": "Missing required field",
  "field": "message"
}
1
2
3
4
5

The middleware parses the HTTP body.

The handler validates application fields.

Test wrong Content-Type

Send JSON to the form route:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/json" \
  --data '{"name":"Gaspard"}'
1
2
3
4

Expected status:

txt
415 Unsupported Media Type
1

The form parser expects:

txt
application/x-www-form-urlencoded
1

If a route expects JSON, use the JSON parser instead.

Test payload too large

The example installs a broad body limit:

cpp
app.use("/contact", middleware::app::body_limit_write_dev(
  16 * 1024
));
1
2
3

and a form parser limit:

cpp
app.use("/contact", middleware::app::form_dev(
  4096
));
1
2
3

To test the form parser limit:

bash
BIG="$(python3 -c 'print("name=Gaspard&email=gaspard@example.com&message=" + "x"*5000)')"

curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "$BIG"
1
2
3
4
5
6

Expected status:

txt
413 Payload Too Large
1

Use body limits and parser limits together:

txt
body limit
  protects the route from large requests

parser limit
  protects the parser from large form bodies
1
2
3
4
5

How it works

The key middleware is:

cpp
app.use("/contact", middleware::app::form_dev(
  4096
));
1
2
3

It checks:

txt
Content-Type
body size
URL-encoded format
1
2
3

Then it stores:

cpp
middleware::parsers::FormBody
1

The route reads it with:

cpp
auto &form =
  req.state<middleware::parsers::FormBody>();
1
2

The fields are available in:

cpp
form.fields
1

which is a map of:

txt
string key
string value
1
2

Middleware order

The example installs middleware in this order:

cpp
app.use("/contact", middleware::app::request_id_dev());
app.use("/contact", middleware::app::timing_dev());
app.use("/contact", middleware::app::security_headers_dev());

app.use("/contact", middleware::app::body_limit_write_dev(
  16 * 1024
));

app.use("/contact", middleware::app::form_dev(
  4096
));
1
2
3
4
5
6
7
8
9
10
11

The order matters:

txt
request id
  identify the request

timing
  measure the request

security headers
  harden the response

body limit
  reject oversized bodies early

form parser
  parse application/x-www-form-urlencoded

handler
  validates fields and returns response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

The body limit should run before the parser.

Why the parser is route-specific

The form parser is installed only on:

cpp
app.use("/contact", ...)
1

not on:

cpp
app.use("/", ...)
1

That matters because most routes do not receive URL-encoded forms.

Good:

cpp
app.use("/contact", middleware::app::form_dev(4096));
1

Risky:

cpp
app.use("/", middleware::app::form_dev(4096));
1

Install parsers only where the route expects that body format.

HTML form example

You can send the same request from a browser form.

html
<form method="post" action="/contact">
  <label>
    Name
    <input name="name" />
  </label>

  <label>
    Email
    <input name="email" type="email" />
  </label>

  <label>
    Message
    <textarea name="message"></textarea>
  </label>

  <button type="submit">Send</button>
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

By default, a normal HTML form like this sends:

txt
application/x-www-form-urlencoded
1

If the form includes file inputs, use:

html
enctype="multipart/form-data"
1

and use the multipart middleware instead.

Form parser vs JSON parser

Use form parser for classic HTML forms:

txt
application/x-www-form-urlencoded
1

Use JSON parser for API clients:

txt
application/json
1

Use multipart parser for files:

txt
multipart/form-data
1

Simple rule:

txt
HTML form without files
  form parser

API request with JSON body
  JSON parser

HTML form with files
  multipart parser
1
2
3
4
5
6
7
8

Add CORS for frontend apps

If a browser frontend runs on another origin, add CORS before the parser:

cpp
app.use("/contact", middleware::app::cors_dev({
  "http://localhost:5173",
  "http://127.0.0.1:5173"
}));

app.use("/contact", middleware::app::body_limit_write_dev(
  16 * 1024
));

app.use("/contact", middleware::app::form_dev(
  4096
));
1
2
3
4
5
6
7
8
9
10
11
12

CORS should run before body parsers so preflight requests can be handled cleanly.

Production notes

For production forms, add:

txt
CSRF protection for browser forms
rate limiting
server-side validation
email format validation
spam protection
logging
request id
clear error responses
1
2
3
4
5
6
7
8

For forms submitted by browsers, CSRF protection is important.

Example middleware shape:

cpp
app.use("/contact", middleware::app::csrf_dev());
app.use("/contact", middleware::app::form_dev(4096));
1
2

The exact setup depends on how your frontend obtains and sends the CSRF token.

Complete test flow

Run:

bash
vix run form_parser.cpp
1

Valid form:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard&email=gaspard@example.com&message=Hello"
1
2
3
4

URL-decoded form:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard+Kirira&email=gaspard%40example.com&message=Hello+from+Vix"
1
2
3
4

Missing field:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard&email=gaspard@example.com"
1
2
3
4

Wrong content type:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/json" \
  --data '{"name":"Gaspard"}'
1
2
3
4

Large body:

bash
BIG="$(python3 -c 'print("name=Gaspard&email=gaspard@example.com&message=" + "x"*5000)')"

curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "$BIG"
1
2
3
4
5
6

Summary

Use the form parser when a route receives:

txt
application/x-www-form-urlencoded
1

The core setup is:

cpp
app.use("/contact", middleware::app::body_limit_write_dev(
  16 * 1024
));

app.use("/contact", middleware::app::form_dev(
  4096
));
1
2
3
4
5
6
7

Then read the parsed form:

cpp
auto &form =
  req.state<middleware::parsers::FormBody>();
1
2

Access fields:

cpp
auto name = form.fields["name"];
1

The mental model is:

txt
body_limit
  protects the route

form_dev
  parses URL-encoded fields

handler
  validates application data
1
2
3
4
5
6
7
8

Released under the MIT License.

Morty Proxy This is a proxified and sanitized view of the page, visit original site.