Accept a vendor media type in a JSON API¶
You’ll build: a content-negotiation helper that accepts requests of
the form Content-Type: application/vnd.acme.v2+json — the pattern
every REST-style API eventually reaches for when it needs to version
without breaking URLs.
You’ll use: polycpp::mime::parse(),
polycpp::mime::format(), and
polycpp::mime::test(), together with the
polycpp::mime::MediaType aggregate.
Prerequisites: read Serve a static file with the right Content-Type first for the basic shape of the API.
Step 1 — validate early, validate once¶
RFC 6838 has strict rules for what’s a legal media type. Rather than
writing your own regex, hand the string to
polycpp::mime::test() at the request edge:
#include <polycpp/mime/mime.hpp>
bool isValidContentType(std::string_view header) {
using namespace polycpp::mime;
// Strip parameters (e.g., "; charset=utf-8") before testing.
auto semi = header.find(';');
auto core = (semi == std::string_view::npos)
? std::string(header)
: std::string(header.substr(0, semi));
return test(core);
}
Reject any request that fails this check with a 415 Unsupported
Media Type — don’t try to “fix” it. Garbage in the request line
almost always indicates a misconfigured client, not a user error.
Step 2 — extract the version from the subtype¶
Once a type passes test, polycpp::mime::parse() splits
it into polycpp::mime::MediaType::type,
polycpp::mime::MediaType::subtype, and
polycpp::mime::MediaType::suffix:
struct VendorVersion {
std::string vendor; // e.g. "acme"
int major; // e.g. 2
std::string format; // e.g. "json"
};
std::optional<VendorVersion> decodeAcme(const MediaType& mt) {
// Expect "application/vnd.acme.v<N>+<fmt>".
if (mt.type != "application") return std::nullopt;
if (mt.suffix.empty()) return std::nullopt;
auto& s = mt.subtype;
if (!s.starts_with("vnd.acme.v")) return std::nullopt;
int n = 0;
for (char c : s.substr(10)) {
if (c < '0' || c > '9') return std::nullopt;
n = n * 10 + (c - '0');
}
return VendorVersion{"acme", n, mt.suffix};
}
parse lowercases its input and trims whitespace, so this comparison
is case-insensitive by default — exactly what the RFC calls for.
Step 3 — respond with a matching content type¶
When you write the response, build the type back up with
polycpp::mime::format(). Round-tripping through the struct
guarantees your output matches what you accepted:
std::string responseContentType(const VendorVersion& v) {
using namespace polycpp::mime;
MediaType mt{
"application",
"vnd." + v.vendor + ".v" + std::to_string(v.major),
v.format,
};
return format(mt); // "application/vnd.acme.v2+json"
}
If any component is invalid, format throws polycpp::TypeError
— treat that as a programmer bug, not a runtime condition.
Step 4 — the full negotiator¶
Chain the steps. The result is short, readable, and has a single place where we fail the request:
#include <polycpp/mime/mime.hpp>
#include <polycpp/core/error.hpp>
struct NegotiationResult {
int status; // 0 on success
std::optional<VendorVersion> version;
std::string responseType;
};
NegotiationResult negotiate(std::string_view header) {
using namespace polycpp::mime;
if (!isValidContentType(header)) return {415, {}, ""};
try {
auto semi = header.find(';');
auto mt = parse(std::string(
(semi == std::string_view::npos)
? header : header.substr(0, semi)));
auto v = decodeAcme(mt);
if (!v) return {415, {}, ""};
return {0, v, responseContentType(*v)};
} catch (const polycpp::TypeError&) {
return {415, {}, ""};
}
}
What you learned¶
testis cheaper thanparse; use it for validation where you don’t need the parts.parselowercases and trims, so you don’t need to pre-normalise.formatis the right way to build a response header — it validates every component so a typo fails fast.The structured-syntax suffix (
+json,+xml) is a first-class field; don’t try to parse it out of the subtype yourself.