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-typeheaders - 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
ExactResponseto 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 bypassessend()and uses the underlying Node response directly:response.writeHead(...)andresponse.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 defaultContent-Type. Werkzeug can also normalize headers for special statuses. The API From Hell implementation createdExactResponse, then explicitly removedContent-Typeprior to sendingresponse.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)andsetHeader("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
httpand Python standardhttp.serverwere easier because they only emit headers we explicitly write. - Mockoon generator copies the catalog
headersarray 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(...)andresponse.end(Buffer.from(body, "utf8")).
- Sent body bytes with the underlying Node response:
-
Python native HTTP
- Avoided JSON/XML serialization.
- Read the catalog body as text, encoded it explicitly with
body.encode("utf-8"), calculatedContent-Lengthfrom the byte array, then wrote bytes withwfile.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
ExactResponseto avoid Werkzeug normalizing response bodies. - Explicitly set
Content-Lengthfrom the byte length.
- Avoided
-
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.
- Converted catalog bodies with
-
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-Lengthregardless ofstatus - 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: