Issue Nº 40 — Sep 23, 2026

The image and Slack formats live on their own paths, not on the Accept header

Issue forty covers icanhazdadjoke, a keyless joke API in the Personality category. This session queried it live. The Accept header only switches between HTML, JSON and plain text. A PNG or a Slack message needs a separate URL. The rate-limit headers also count each route on its own, not as one shared total.

Accept only picks HTML, JSON, or plain text

This session sent GET https://icanhazdadjoke.com/ with curl's default header, Accept: */*. It returned 200 and text/plain, one joke as bare text. The same request with the Accept header removed completely returned text/html instead.

The same URL with a browser-style Accept header (text/html first) returned 200 and text/html, the full docs-style page markup, not a joke by itself.

This session then sent Accept: image/png to the same URL. It still returned text/html. The Accept header cannot select an image on the root path.

GET https://icanhazdadjoke.com/j/{id}.png, a separate path with the id and a .png suffix, returned 200 and image/png, a real PNG image of the joke. The image only comes from that dedicated path.

The docs list three Accept values: text/html, application/json, and text/plain. They say curl without an Accept header gets text/plain. In fact curl sends Accept: */*, and that is the value that gets plain text. A request with no Accept header at all gets HTML, and so does image/png.

The Slack format is a separate endpoint, not a fourth Accept value

This session sent Accept: text/slack to the root path. It returned the default text/html page, not a Slack message. Content negotiation does not cover Slack.

GET https://icanhazdadjoke.com/slack, sent with curl's default Accept: */*, returned 200 and application/json. The body was a Slack attachments payload: fallback text, the joke text, and a footer with a permalink. It also set response_type to in_channel.

The API's own docs confirm this: the Slack format has its own heading and its own URL, separate from the Accept-header table.

A client that wants a Slack-formatted joke must call /slack directly. Accept: text/slack on the root path does not give one.

Each route keeps its own rate-limit count

This session sent two GET requests in a row to each of four routes, all with Accept: application/json. The root path returned x-ratelimit-remaining 95, then 94, because it had taken earlier requests in this session.

In the same seconds, /j/aaa1 (a missing joke id) returned 99, then 98. /search returned 99, then 98. /slack returned 99, then 98. Each route also had its own x-ratelimit-reset value. The headers show a separate counter for each route, not one total for all traffic.

GET https://icanhazdadjoke.com/j/doesnotexist12345 returned HTTP 200 with a 404 status inside the JSON body. It carried the x-ratelimit-limit, x-ratelimit-remaining, and x-ratelimit-reset headers.

GET https://icanhazdadjoke.com/nope, a path the API does not know, returned a real HTTP 404. That response carried no x-ratelimit headers. Both error responses still carried access-control-allow-origin: *, so a browser can read them.

The /search endpoint clamps bad values and does not fail. limit=31 came back with limit:30, the documented cap. limit=0 came back with limit:1. page=9999 returned an empty results array, and total_pages still gave the true count.

icanhazdadjoke, by the numbers

Rendered live from the atlas entry
AuthenticationNone required
HTTPSSupported
CORSEnabled
PricingFree
FormatsJSON
Rate limit

100 requests per IP per fixed ~60s window, anchored at the window's first request (measured live via x-ratelimit-limit/-remaining/-reset response headers; not documented in the API's own docs)

jokeshumordad-jokesfun

Sources

Facts checked Sep 2026