Skip to main content
How to Write an API From Hell

10 minute read - API Testing API Challenges

How to Write an API From Hell

Published: Aug 20, 2026

TLDR; the API From Hell has multiple implementations each of which required bypassing the language and frameworks used in different ways and now I can stress test REST Clients and I’ve found some error conditions in HTTP Proxies.

One of the interesting aspects of building the API From Hell over on API Challenges was having to bypass default library and framework HTTP processing.

What is the API From Hell?

The API From Hell is an HTTP server application with a set of endpoints, each of which responds with a fixed response that is either:

  • good - to demonstrate differnt API Tooling rendering capabilities
  • problematic - has something that might cause a downstream API response processor some issues, but should work in the API client
  • error - has something that might cause the client issues

Prototype to Working App

I originally started writing it using Mockoon as an easy way to protptype the concept.

Mockoon is build on normal HTTP frameworks and there were certain erroneous conditions that I wanted to create that Mockoon would filter out when sending the request.

Mockoon is still part of the API From Hell standalone implementation but cannot generate all the error conditions.

I then hard coded it in Java using Javalin. And for some concepts I had to go a little ‘below’ the framework.

After I released it to the wild on API Challenges I found that the ‘internet’ i.e. the Hosting Server, CDNs etc. all made it impossible to send some responses in the wild.

Try it here:

So to get the full range of Hellish API Responses you have to run it locally. And… to make it easier to run locally I created multiple implementations in different languages, available from Github.

The hardest conditions to generate so far were

  • 204, 205 and 304 - no content - with content in the body
  • Missing Content-type headers
  • BOMs, Unicode, null bytes and control characters

Some Tools Need to Bypass Frameworks

Part of the reason I wanted multiple implementation was because I wanted to know how much each of the languages and frameworks contributed to not handling these low level conditions.

When you’re implementing an app that makes calls to an API and receives responses you pretty much want the framework to handle most of this for you, and if you get a 204 with a body, you probably don’t care that there is a body and you probably won’t read it anyway.

But… if you’re writing a tool that helps people test APIs e.g. an HTTP Client, an HTTP library, a REST API Client, a REST API Test Tool, an HTTP Proxy - then you are going to be vulnerable to the lower level libraries unless you bypass them.

The API From Hell has helped me see which tools do not bypass those libraries and so are vulnerable and can not offer the full range of functionality that I would expect.

e.g.

  • Postman does not process or render the 204 and 304 messages when they have content
  • MITMProxy and Zap proxy tools strip out the content from 204 and 304 and doe not pass it to the downstream system
  • Bruno ‘fixes’ broken XML and renders it as if it were correct
  • I’m still working through other tools so I don’t know what issues I’ll find

I decided to stop using MITMProxy and ZAP as a result of this for most observational work.

I need a proxy to work at a lower, raw, HTTP and Socket level so that the exact message is received and proxied. If I want the message amended then I configure the proxy to do so, by default, I need an accurate proxy.

204, 205, 304

  • 204, 205 and 304 responses - typically don’t have any body content, so the error was adding body content. This was unexpectedly hard.

HTTP Clients don’t like to send body content if you have a 204. 205 or 304 status and typically strip the content off. Both during sending and receipt.

The fact that the condition is hard to trigger is good news because it means that your APIs probably aren’t sending a 204 or 304 with body content. And even if they do, the downstream systems probably won’t see it when their client processes it.

But… if you are able to send it then it might well cause downstream systems some issues.

To explore this further I created stand alone implementations of the API from Hell using multiple languages - all driven from a single JSON representation of the API.

  • Node native and Express write the status line, headers, Content-Length, and body directly to the socket for those statuses.
  • Flask needed a custom ExactResponse to stop Werkzeug (he lower-level Python web library that Flask is built on) from stripping headers or body data.
  • The embedded API Challenges route in Javelin uses response.forceBody(...) for 204/304 cases. Unwraps the Jetty request, gets the raw endpoint, writes a complete HTTP response byte array, and closes the connection.
  • The standalone Java implementation ultimately became a raw socket HTTP server so no intermediate framework would normalize the response away.

Sometimes its hard to make mistakes… but if you’re going to test something then you need to get the right data into the system.

Missing Content Type

Missing Content-Type was another condition that required fighting with the framework. Some libraries eagerly add a default content type, so the code had to explicitly suppress or remove it. The embedded adapter also strips unwanted ;charset=utf-8 suffixes so catalogued header values stay exact. Http Servers will also add this, CDNs will add this, so running the API from Hell locally is the only way to guarantee that you’ll see no Content-Type.

The catalog deliberately defines bodies with no Content-Type for:

  • /fromhell/version
  • /fromhell/missing-content-type/json
  • /fromhell/missing-content-type/xml
  • /fromhell/missing-content-type/html

Libraries that had to be bypassed:

  • Express is too helpful. If you send a string body, Express will infer a content type, often text/html; charset=utf-8. The API From Hell Express implementation bypasses send() and uses the underlying Node response directly: response.writeHead(...) and response.end(Buffer.from(...)), with only the headers from the catalog/common CORS headers.
  • Flask 3 / Werkzeug: flask.Response(...) starts with default response metadata, so a plain body will end up with a default Content-Type. Werkzeug can also normalize headers for special statuses. The API From Hell implementation created ExactResponse, then explicitly removed Content-Type prior to sending response.headers.pop("Content-Type", None).
  • Javalin 7.2.2 / Jetty 12.1.8 / Jakarta Servlet 6.0.0 in the embedded API Challenges version: the Javalin/Jetty response stack can restore/default response typing or append charset handling after route code runs. The API Challenges implementation calls the servlet response with setContentType(null) and setHeader("Content-Type", null).
  • Standalone Java: the java-raw-http. For exact bad responses, raw sockets avoid framework defaults entirely. The Java raw implementation writes the status line, catalog headers, Content-Length, and body bytes itself.

There were a few easier implementations:

  • Node native http and Python standard http.server were easier because they only emit headers we explicitly write.
  • Mockoon generator copies the catalog headers array directly, so for missing content type it generates no header.

BOMs, Unicode, null bytes, and control characters

BOMs, Unicode, null bytes, and control characters had to be treated as bytes, not parsed objects. The catalog includes examples such as:

  • JSON BOM prefix: first bytes ef bb bf
  • XML BOM prefix: first bytes ef bb bf
  • Octet-stream control payload containing 00 01 02
  • Malformed JSON/XML bodies containing literal 0x01
  • Unicode edge cases such as zero-width and confusable characters

Take the predefined body, encode it as UTF-8, send those bytes, and do not let a JSON/XML/content-type abstraction reinterpret it.

  • Node.js native HTTP

    • Avoided JSON/string response helpers.
    • Encoded catalog bodies with Buffer.from(body, "utf8").
    • Wrote body bytes with response.end(...).
  • Node.js Express

    • Sent body bytes with the underlying Node response: response.writeHead(...) and response.end(Buffer.from(body, "utf8")).
  • Python native HTTP

    • Avoided JSON/XML serialization.
    • Read the catalog body as text, encoded it explicitly with body.encode("utf-8"), calculated Content-Length from the byte array, then wrote bytes with wfile.write(body).
    • This preserved BOM bytes, NUL bytes, and raw control characters exactly as catalogued.
  • Python Flask

    • Avoided jsonify(...) and normal high-level response behavior.
    • Encoded the catalog body to UTF-8 bytes before constructing the response.
    • Added a custom ExactResponse to avoid Werkzeug normalizing response bodies.
    • Explicitly set Content-Length from the byte length.
  • Java raw HTTP

    • Converted catalog bodies with getBytes(StandardCharsets.UTF_8).
    • Wrote body bytes directly to the socket, preserving BOMs, NULs, control characters, and illegal status/body combinations.
  • Embedded API Challenges / Javalin

    • Avoided JSON/XML serializers for API From Hell endpoint bodies.
    • Used lower-level response to use Jetty direct and sends the UTF-8 body bytes using EndPoint.write(...)

Testing This

The generic automated coverage for the APIs uses Python and in order to process the responses we had to bypass the normal HTTP response processing. If the tool you use for testing does not do this then you will not see the full actual response the API sends.

So the Python test client uses raw sockets:

  • It writes its own HTTP request bytes.
  • It reads the full response until the server closes the connection.
  • It parses the status line and headers without a framework.
  • It decodes chunked bodies when needed.
  • It honors Content-Length regardless of status
  • It supports HTTP proxy mode by using absolute-form request targets, and HTTPS proxy mode via CONNECT.

So it is a much lower level implementation than a normal API client for automating would use - but it is the level that I would expect from an API Testing Tool or Proxy.

Assertions are based on strict byte equality rather than parsed message body.

expected_body = endpoint_body(endpoint).encode("utf-8")

Try automating it yourself

The API From Hell is difficult to work with because most HTTP tooling is designed to prevent exactly the sort of issues we are generating.

If you want to automate then.

  • Avoid parsing bad JSON/XML as JSON/XML.
  • Preserve bodies as UTF-8 bytes.
  • Bypass framework handling for impossible status/body combinations.
  • Add raw HTTP clients because higher level clients normalize or reject the responses.
  • Compare exact bytes, not “equivalent” text.
  • Use proxy support to inspect what actually reaches a client.

You can run whichever of the standalone implementations makes sense to you, they are all compliant with the full test suite (except Mockoon).

Proxies

But note… proxies might also be vulnerable to this type of behaviour.

I’ve tried the API From Hell with:

  • BurpSuite
  • MITMProxy
  • Zap

Both Zap and MITMProxy failed to proxy through the 204 and 304 no content body payloads. And both failed to show the body in the actual proxy tool itself.

Only BurpSuite so far, has proxied the full set of API From Hell payloads and headers.

Revealing

To me, this helps reveal the hidden assumptions that we make when using tools.

We assume that these tools are working, but it is only when we stress them that we find out.

I expected to find issues with REST Clients and HTTP Libraries.

I did not expect to find issues with HTTP Proxies.

I usually takes me about an hour to run through the full API From Hell in a REST Client - from loading in the Open API file, setting up a Proxy (now I only use BurpSuite). Then figuring out how the REST Client has changed and what I need to do to actually send a request and see the response.

But I am preparing a set of videos because the API From Hell is now part of my review process for any REST Client that I look at.

Try it here on the cloud, but remember the Internet infrastructure gets in the way of some responses:

Run it locally to get the full range of Hellish API Responses:

If you like this content then you might be interested in my Patreon Community. I create exclusive content multiple times a week. Gain access to Patreon only content and online training courses for as little as $1 per month. Learn more about the EvilTester Patreon Community.

<< A Trail of Code - Boomstash