Combine HTTP Cat and HTTP Dog APIs to build an interactive, image-rich gallery that turns the HTTP specification into a delightful educational experience for developers of all levels.
Every web developer encounters HTTP status codes daily, yet few can explain the difference between a 502 and a 504, or tell you what a 418 means without looking it up. Status codes are the vocabulary of the web. They are how servers communicate with clients, how APIs signal success or failure, and how browsers decide what to show the user. Yet they are taught as dry reference tables, memorized through rote repetition, and forgotten within days.
What if learning HTTP status codes was fun? What if each code came with a picture of a cat and a dog, a clear explanation of when you would encounter it, and a real-world scenario that makes the abstract concrete? That is the premise of HTTP Zoo, an interactive gallery that transforms the HTTP specification into a visual, explorable, and genuinely entertaining experience.
We will combine two of the internet's most beloved APIs: HTTP Cat (which returns a cat image for every HTTP status code) and HTTP Dog (which does the same but with dogs). For each code, the user sees both images side by side, along with a detailed explanation, common causes, debugging tips, and the relevant section of the HTTP specification. The result is a tool that is useful enough to be bookmarked by experienced developers and accessible enough to teach beginners.
This project is deceptively educational. Beneath the cute animal pictures lies a comprehensive reference to the HTTP protocol, one of the most important and least understood technologies that powers the web. By the end of this article, you will not only have built a delightful gallery but also achieved a deep understanding of how HTTP status codes work, why they exist, and what they mean for your applications.
HTTP status codes are three-digit numbers organized into five families, each identified by its leading digit. Understanding these families is the key to understanding any status code you encounter, even if you have never seen that specific code before.
1xx (Informational): These are provisional responses. The server has received the request and is processing it. You rarely see these in everyday development, but they are important for protocols like WebSocket upgrades (101 Switching Protocols) and large file uploads (100 Continue). They exist to keep the connection alive and signal that work is happening.
2xx (Success): The request was received, understood, and accepted. The most common is 200 OK, but there are important nuances. 201 Created means a new resource was created (the correct response after a successful POST). 204 No Content means the request succeeded but there is nothing to send back (common after a DELETE). 206 Partial Content means only part of the resource was returned (used in range requests for streaming video).
3xx (Redirection): The client needs to take additional action to complete the request. 301 Moved Permanently means the resource has a new URL and all future requests should use it. 302 Found (often misused) means the resource is temporarily at a different URL. 304 Not Modified means the cached version is still valid, saving bandwidth and time.
4xx (Client Error): The request contains an error that the client should fix. 400 Bad Request means the syntax is wrong. 401 Unauthorized means authentication is required. 403 Forbidden means authentication succeeded but the user lacks permission. 404 Not Found means the resource does not exist. 429 Too Many Requests means the client is being rate limited.
5xx (Server Error): The server failed to fulfill a valid request. 500 Internal Server Error is the catch-all for server-side failures. 502 Bad Gateway means the server, acting as a proxy, received an invalid response from the upstream server. 503 Service Unavailable means the server is temporarily overloaded or under maintenance. 504 Gateway Timeout means the proxy server did not receive a timely response from the upstream server.
Our gallery needs a comprehensive dataset of all HTTP status codes, including official codes defined in RFCs, common unofficial codes, and the legendary joke codes. Here is the data structure we will use.
const httpStatusCodes = [
// 1xx Informational
{
code: 100, name: 'Continue',
family: '1xx', familyName: 'Informational',
description: 'The server has received the request headers and the client should proceed to send the request body.',
commonCause: 'Client sent Expect: 100-continue header before uploading a large request body.',
debugTip: 'Usually transparent. Your HTTP client handles this automatically.',
rfcSection: 'RFC 7231, Section 6.2.1'
},
{
code: 101, name: 'Switching Protocols',
family: '1xx', familyName: 'Informational',
description: 'The server is switching protocols as requested by the client via the Upgrade header.',
commonCause: 'WebSocket connection upgrade from HTTP to the WebSocket protocol.',
debugTip: 'Check the Upgrade and Connection headers. Ensure the server supports the requested protocol.',
rfcSection: 'RFC 7231, Section 6.2.2'
},
{
code: 102, name: 'Processing',
family: '1xx', familyName: 'Informational',
description: 'The server has received and is processing the request, but no response is available yet.',
commonCause: 'WebDAV: server is performing a complex operation that takes time.',
debugTip: 'This is a keep-alive signal. If it takes too long, check server-side processing.',
rfcSection: 'RFC 2518, Section 10.1'
},
{
code: 103, name: 'Early Hints',
family: '1xx', familyName: 'Informational',
description: 'Used to return some response headers before the final response.',
commonCause: 'Server sends Link headers early so the browser can preload resources.',
debugTip: 'Check for Link: rel=preload headers. Useful for performance optimization.',
rfcSection: 'RFC 8297'
},
// 2xx Success
{
code: 200, name: 'OK',
family: '2xx', familyName: 'Success',
description: 'The request has succeeded. The meaning depends on the HTTP method used.',
commonCause: 'Standard successful response for GET, POST, PUT, PATCH requests.',
debugTip: 'If you expected different data, check the request parameters and server logic.',
rfcSection: 'RFC 7231, Section 6.3.1'
},
{
code: 201, name: 'Created',
family: '2xx', familyName: 'Success',
description: 'The request has been fulfilled and a new resource has been created.',
commonCause: 'Successful POST request that creates a new resource (user, record, file).',
debugTip: 'The response should include a Location header with the URL of the new resource.',
rfcSection: 'RFC 7231, Section 6.3.2'
},
{
code: 204, name: 'No Content',
family: '2xx', familyName: 'Success',
description: 'The server successfully processed the request but is not returning any content.',
commonCause: 'Successful DELETE request. Also used for PUT/PATCH when no response body is needed.',
debugTip: 'Do not expect a response body. If you need confirmation data, use 200 instead.',
rfcSection: 'RFC 7231, Section 6.3.5'
},
{
code: 206, name: 'Partial Content',
family: '2xx', familyName: 'Success',
description: 'The server is delivering only part of the resource due to a range header sent by the client.',
commonCause: 'Video/audio streaming, resume download after interruption, PDF page-by-page loading.',
debugTip: 'Check the Content-Range response header for the byte range delivered.',
rfcSection: 'RFC 7233, Section 4.1'
},
// 3xx Redirection
{
code: 301, name: 'Moved Permanently',
family: '3xx', familyName: 'Redirection',
description: 'The resource has been permanently moved to a new URL. All future requests should use the new URL.',
commonCause: 'Domain migration, HTTP-to-HTTPS redirect, URL restructuring.',
debugTip: 'Check the Location header for the new URL. Update bookmarks and links.',
rfcSection: 'RFC 7231, Section 6.4.2'
},
{
code: 302, name: 'Found',
family: '3xx', familyName: 'Redirection',
description: 'The resource is temporarily located at a different URL. Future requests should still use the original URL.',
commonCause: 'Temporary redirect during maintenance, A/B testing, geographic routing.',
debugTip: 'Often confused with 301. Use 301 for permanent moves, 302 for temporary.',
rfcSection: 'RFC 7231, Section 6.4.3'
},
{
code: 304, name: 'Not Modified',
family: '3xx', familyName: 'Redirection',
description: 'The resource has not been modified since the last request. The client can use the cached version.',
commonCause: 'Browser sends If-Modified-Since or If-None-Match headers, and the resource has not changed.',
debugTip: 'This is a performance win. It means caching is working correctly.',
rfcSection: 'RFC 7232, Section 4.1'
},
{
code: 307, name: 'Temporary Redirect',
family: '3xx', familyName: 'Redirection',
description: 'Like 302, but the request method must not change. A POST redirect stays POST.',
commonCause: 'Redirecting a form submission to a different processing endpoint.',
debugTip: 'Prefer 307 over 302 when you need the HTTP method preserved.',
rfcSection: 'RFC 7231, Section 6.4.7'
},
{
code: 308, name: 'Permanent Redirect',
family: '3xx', familyName: 'Redirection',
description: 'Like 301, but the request method must not change. A POST redirect stays POST.',
commonCause: 'Permanently redirecting an API endpoint while preserving the request method.',
debugTip: 'Prefer 308 over 301 when you need the HTTP method preserved.',
rfcSection: 'RFC 7538'
},
// 4xx Client Errors
{
code: 400, name: 'Bad Request',
family: '4xx', familyName: 'Client Error',
description: 'The server cannot process the request due to something perceived to be a client error.',
commonCause: 'Malformed JSON, missing required fields, invalid query parameters.',
debugTip: 'Check the request body syntax, required fields, and content-type header.',
rfcSection: 'RFC 7231, Section 6.5.1'
},
{
code: 401, name: 'Unauthorized',
family: '4xx', familyName: 'Client Error',
description: 'The request requires authentication. The client must authenticate itself to get the requested response.',
commonCause: 'Missing or expired auth token, invalid API key, session timeout.',
debugTip: 'Check Authorization header. Ensure token is valid and not expired.',
rfcSection: 'RFC 7235, Section 3.1'
},
{
code: 403, name: 'Forbidden',
family: '4xx', familyName: 'Client Error',
description: 'The client is authenticated but does not have permission to access the requested resource.',
commonCause: 'Insufficient permissions, IP blocking, geographic restriction, CORS preflight failure.',
debugTip: 'Authentication succeeded but authorization failed. Check user roles and permissions.',
rfcSection: 'RFC 7231, Section 6.5.3'
},
{
code: 404, name: 'Not Found',
family: '4xx', familyName: 'Client Error',
description: 'The server cannot find the requested resource. The URL may be wrong or the resource may have been deleted.',
commonCause: 'Typo in URL, deleted resource, missing route handler, wrong API version.',
debugTip: 'Double-check the URL spelling, path parameters, and API base URL.',
rfcSection: 'RFC 7231, Section 6.5.4'
},
{
code: 405, name: 'Method Not Allowed',
family: '4xx', familyName: 'Client Error',
description: 'The HTTP method used is not supported for the requested resource.',
commonCause: 'Sending POST to a GET-only endpoint, or DELETE to a read-only resource.',
debugTip: 'Check the Allow response header for the list of supported methods.',
rfcSection: 'RFC 7231, Section 6.5.5'
},
{
code: 408, name: 'Request Timeout',
family: '4xx', familyName: 'Client Error',
description: 'The server timed out waiting for the request from the client.',
commonCause: 'Slow client connection, client paused mid-request, network issues.',
debugTip: 'Check network connectivity. Consider increasing timeout values.',
rfcSection: 'RFC 7231, Section 6.5.7'
},
{
code: 409, name: 'Conflict',
family: '4xx', familyName: 'Client Error',
description: 'The request conflicts with the current state of the server.',
commonCause: 'Duplicate resource creation, editing a stale version, conflicting constraints.',
debugTip: 'Re-fetch the resource to get the latest state before retrying.',
rfcSection: 'RFC 7231, Section 6.5.8'
},
{
code: 418, name: "I'm a Teapot",
family: '4xx', familyName: 'Client Error',
description: 'The server refuses to brew coffee because it is a teapot. This is an April Fools joke from 1998.',
commonCause: 'Defined in the Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0).',
debugTip: 'If you see this in production, someone has a sense of humor. Enjoy it.',
rfcSection: 'RFC 2324, Section 2.3.2'
},
{
code: 422, name: 'Unprocessable Entity',
family: '4xx', familyName: 'Client Error',
description: 'The request was well-formed but could not be processed due to semantic errors.',
commonCause: 'Validation failure: correct JSON syntax but invalid field values.',
debugTip: 'Check the response body for specific validation error messages.',
rfcSection: 'RFC 4918, Section 11.2'
},
{
code: 429, name: 'Too Many Requests',
family: '4xx', familyName: 'Client Error',
description: 'The client has sent too many requests in a given amount of time (rate limiting).',
commonCause: 'Exceeded API rate limit, aggressive polling, missing request throttling.',
debugTip: 'Check Retry-After header. Implement exponential backoff.',
rfcSection: 'RFC 6585, Section 4'
},
// 5xx Server Errors
{
code: 500, name: 'Internal Server Error',
family: '5xx', familyName: 'Server Error',
description: 'The server encountered an unexpected condition that prevented it from fulfilling the request.',
commonCause: 'Unhandled exception, database connection failure, null pointer, misconfiguration.',
debugTip: 'Check server logs. This is always a server-side bug, never a client issue.',
rfcSection: 'RFC 7231, Section 6.6.1'
},
{
code: 502, name: 'Bad Gateway',
family: '5xx', familyName: 'Server Error',
description: 'The server, acting as a gateway or proxy, received an invalid response from an upstream server.',
commonCause: 'Upstream server crashed, invalid response format, DNS resolution failure.',
debugTip: 'Check if the upstream server is running. Review proxy/load balancer configuration.',
rfcSection: 'RFC 7231, Section 6.6.3'
},
{
code: 503, name: 'Service Unavailable',
family: '5xx', familyName: 'Server Error',
description: 'The server is temporarily unable to handle the request due to maintenance or overload.',
commonCause: 'Planned maintenance, traffic spike, resource exhaustion, deployment in progress.',
debugTip: 'Check Retry-After header. Usually resolves on its own. Check server health metrics.',
rfcSection: 'RFC 7231, Section 6.6.4'
},
{
code: 504, name: 'Gateway Timeout',
family: '5xx', familyName: 'Server Error',
description: 'The server, acting as a gateway or proxy, did not receive a timely response from the upstream server.',
commonCause: 'Upstream server too slow, network latency, database query timeout.',
debugTip: 'Check upstream server response times. Consider increasing proxy timeout values.',
rfcSection: 'RFC 7231, Section 6.6.5'
}
];
HTTP Cat is one of the internet's most beloved developer tools. Created by Tomomi Imura, it maps every HTTP status code to a photograph of a cat in a situation that humorously illustrates the code's meaning. The API is dead simple: you construct a URL with the status code, and it returns a JPEG image.
// HTTP Cat API
// Returns a cat image for any HTTP status code
// https://http.cat/{status_code}
// Examples:
// https://http.cat/200 -> Cat looking satisfied
// https://http.cat/404 -> Cat looking confused
// https://http.cat/500 -> Cat looking panicked
// https://http.cat/418 -> Cat with a teapot
// Usage in HTML:
// <img src="https://http.cat/404" alt="404 Not Found Cat" />
The beauty of this API is that it requires no fetch calls, no JSON parsing, no authentication. You simply set an image's src attribute to the URL and the browser handles the rest. The images are served with appropriate caching headers, so repeat views are instant. Not all status codes have images; some obscure codes return a generic placeholder. We will handle missing images gracefully with error event listeners.
HTTP Dog follows the same pattern as HTTP Cat but with photographs of dogs. The coverage is extensive, with images for all standard HTTP status codes and many extended codes.
// HTTP Dog API
// Returns a dog image for any HTTP status code
// https://http.dog/{status_code}.jpg
// Examples:
// https://http.dog/200.jpg -> Happy dog
// https://http.dog/404.jpg -> Searching dog
// https://http.dog/503.jpg -> Tired dog
// Note the .jpg extension - HTTP Dog requires it, HTTP Cat does not
There is one subtle difference: HTTP Dog URLs require the .jpg extension while HTTP Cat URLs do not. This is a minor inconsistency that you need to account for when building the image URLs. Both APIs serve JPEG images and support CORS, so there are no cross-origin issues.
The images are the hook, but the education is the substance. Our data model for each status code includes not just the code and name but also a plain-English description, common causes, debugging tips, and RFC references. This transforms the gallery from a novelty into a genuine reference tool that developers will return to when they encounter an unfamiliar status code in their logs.
The gallery renders as a responsive grid of cards. Each card shows the status code, its name, both animal images, and an expandable details section. The cards are grouped by family (1xx, 2xx, etc.) with sticky headers for easy navigation.
function renderGallery(codes, filters) {
const container = document.getElementById('gallery');
const filteredCodes = applyFilters(codes, filters);
const grouped = groupByFamily(filteredCodes);
const familyColors = {
'1xx': '#45aaf2', // Blue - Informational
'2xx': '#26de81', // Green - Success
'3xx': '#fed330', // Yellow - Redirection
'4xx': '#ff6b6b', // Red - Client Error
'5xx': '#a55eea' // Purple - Server Error
};
let html = '';
for (const [family, codes] of Object.entries(grouped)) {
const color = familyColors[family];
html += `
<div class="family-section">
<h2 class="family-header" style="border-left: 4px solid ${color}">
<span class="family-code" style="color: ${color}">${family}</span>
${codes[0].familyName}
</h2>
<div class="cards-grid">
${codes.map(code => renderStatusCard(code, color)).join('')}
</div>
</div>
`;
}
container.innerHTML = html;
attachImageErrorHandlers();
}
function renderStatusCard(statusCode, familyColor) {
return `
<div class="status-card" id="code-${statusCode.code}" data-code="${statusCode.code}">
<div class="card-header" style="border-top: 3px solid ${familyColor}">
<span class="code-number" style="color: ${familyColor}">
${statusCode.code}
</span>
<span class="code-name">${statusCode.name}</span>
</div>
<div class="animal-images">
<div class="animal-image">
<img src="https://http.cat/${statusCode.code}"
alt="${statusCode.code} ${statusCode.name} Cat"
loading="lazy"
onerror="this.parentElement.classList.add('no-image')" />
<span class="animal-label">Cat</span>
</div>
<div class="animal-image">
<img src="https://http.dog/${statusCode.code}.jpg"
alt="${statusCode.code} ${statusCode.name} Dog"
loading="lazy"
onerror="this.parentElement.classList.add('no-image')" />
<span class="animal-label">Dog</span>
</div>
</div>
<div class="card-description">
<p>${statusCode.description}</p>
</div>
<button class="details-toggle" onclick="toggleDetails(${statusCode.code})">
Show Details
</button>
<div class="card-details" id="details-${statusCode.code}" hidden>
<div class="detail-section">
<h4>Common Causes</h4>
<p>${statusCode.commonCause}</p>
</div>
<div class="detail-section">
<h4>Debugging Tip</h4>
<p>${statusCode.debugTip}</p>
</div>
<div class="detail-section">
<h4>Specification</h4>
<p>${statusCode.rfcSection}</p>
</div>
</div>
</div>
`;
}
function toggleDetails(code) {
const details = document.getElementById(`details-${code}`);
const btn = details.previousElementSibling;
details.hidden = !details.hidden;
btn.textContent = details.hidden ? 'Show Details' : 'Hide Details';
}
function groupByFamily(codes) {
return codes.reduce((groups, code) => {
const family = code.family;
if (!groups[family]) groups[family] = [];
groups[family].push(code);
return groups;
}, {});
}
A gallery is only as useful as its search functionality. We implement both a text search (searching code numbers, names, and descriptions) and family filters (show only 4xx errors, for example).
const filters = {
search: '',
families: new Set(['1xx', '2xx', '3xx', '4xx', '5xx']),
animalPreference: 'both' // 'cat', 'dog', or 'both'
};
function applyFilters(codes, filters) {
return codes.filter(code => {
// Family filter
if (!filters.families.has(code.family)) return false;
// Text search
if (filters.search) {
const query = filters.search.toLowerCase();
const searchable = [
String(code.code),
code.name.toLowerCase(),
code.description.toLowerCase(),
code.commonCause.toLowerCase(),
code.debugTip.toLowerCase()
].join(' ');
if (!searchable.includes(query)) return false;
}
return true;
});
}
function setupFilterHandlers() {
// Search input
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', debounce((e) => {
filters.search = e.target.value;
renderGallery(httpStatusCodes, filters);
}, 200));
// Family toggle buttons
document.querySelectorAll('.family-filter').forEach(btn => {
btn.addEventListener('click', () => {
const family = btn.dataset.family;
if (filters.families.has(family)) {
filters.families.delete(family);
btn.classList.remove('active');
} else {
filters.families.add(family);
btn.classList.add('active');
}
renderGallery(httpStatusCodes, filters);
});
});
// Animal preference toggle
document.querySelectorAll('.animal-toggle').forEach(btn => {
btn.addEventListener('click', () => {
filters.animalPreference = btn.dataset.animal;
document.querySelectorAll('.animal-toggle').forEach(b =>
b.classList.toggle('active', b.dataset.animal === filters.animalPreference)
);
renderGallery(httpStatusCodes, filters);
});
});
}
function debounce(fn, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), delay);
};
}
For developers who know exactly which code they need, we provide a quick lookup input that immediately scrolls to and highlights the relevant card. This is implemented with a hash-based URL scheme so that http-zoo.html#418 links directly to the teapot.
function setupQuickLookup() {
const input = document.getElementById('quickLookup');
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const code = parseInt(input.value, 10);
if (code && code >= 100 && code <= 599) {
scrollToCode(code);
}
}
});
// Handle URL hash on page load
const hash = window.location.hash.replace('#', '');
if (hash && !isNaN(parseInt(hash))) {
setTimeout(() => scrollToCode(parseInt(hash)), 500);
}
}
function scrollToCode(code) {
const card = document.getElementById(`code-${code}`);
if (card) {
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
card.classList.add('highlighted');
setTimeout(() => card.classList.remove('highlighted'), 3000);
window.location.hash = code;
// Auto-expand details
const details = document.getElementById(`details-${code}`);
if (details && details.hidden) {
toggleDetails(code);
}
} else {
showNotification(`Status code ${code} is not in the gallery.`);
}
}
With 50+ status codes and two images each, that is 100+ images. We cannot load them all eagerly. Our strategy uses native lazy loading combined with an Intersection Observer for preloading images that are about to come into view.
function setupImagePreloading() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const card = entry.target;
const images = card.querySelectorAll('img[data-src]');
images.forEach(img => {
img.src = img.dataset.src;
img.removeAttribute('data-src');
});
observer.unobserve(card);
}
});
},
{
rootMargin: '300px 0px', // Start loading 300px before visible
threshold: 0
}
);
document.querySelectorAll('.status-card').forEach(card => {
observer.observe(card);
});
}
function attachImageErrorHandlers() {
document.querySelectorAll('.animal-image img').forEach(img => {
img.addEventListener('error', function() {
this.style.display = 'none';
const parent = this.parentElement;
if (!parent.querySelector('.no-image-msg')) {
const msg = document.createElement('div');
msg.className = 'no-image-msg';
msg.textContent = 'No image available';
parent.appendChild(msg);
}
});
});
}
To reinforce learning, we include a quiz mode that shows an animal image and asks the user to identify the status code. This gamified approach is far more effective than rote memorization.
function startQuiz() {
const quizState = {
currentQuestion: 0,
score: 0,
totalQuestions: 10,
questions: generateQuizQuestions(10),
answered: false
};
renderQuiz(quizState);
}
function generateQuizQuestions(count) {
const codes = [...httpStatusCodes].sort(() => Math.random() - 0.5);
return codes.slice(0, count).map(code => {
// Generate 3 wrong answers from the same family
const sameFamily = httpStatusCodes.filter(
c => c.family === code.family && c.code !== code.code
);
const otherCodes = httpStatusCodes.filter(c => c.family !== code.family);
const wrongAnswers = [...sameFamily, ...otherCodes]
.sort(() => Math.random() - 0.5)
.slice(0, 3);
const options = [code, ...wrongAnswers].sort(() => Math.random() - 0.5);
const usesCat = Math.random() > 0.5;
return {
correctCode: code,
options: options,
imageUrl: usesCat
? `https://http.cat/${code.code}`
: `https://http.dog/${code.code}.jpg`,
animalType: usesCat ? 'cat' : 'dog'
};
});
}
function renderQuiz(quizState) {
const container = document.getElementById('quizContainer');
const q = quizState.questions[quizState.currentQuestion];
container.innerHTML = `
<div class="quiz-progress">
Question ${quizState.currentQuestion + 1} of ${quizState.totalQuestions}
| Score: ${quizState.score}
</div>
<div class="quiz-question">
<img src="${q.imageUrl}" alt="Mystery HTTP status code ${q.animalType}"
class="quiz-image" />
<p>Which HTTP status code does this ${q.animalType} represent?</p>
</div>
<div class="quiz-options">
${q.options.map(opt => `
<button class="quiz-option" data-code="${opt.code}"
onclick="checkAnswer(${opt.code}, ${q.correctCode.code})">
<span class="option-code">${opt.code}</span>
<span class="option-name">${opt.name}</span>
</button>
`).join('')}
</div>
`;
}
function checkAnswer(selectedCode, correctCode) {
const buttons = document.querySelectorAll('.quiz-option');
buttons.forEach(btn => {
btn.disabled = true;
const code = parseInt(btn.dataset.code);
if (code === correctCode) btn.classList.add('correct');
if (code === selectedCode && code !== correctCode) btn.classList.add('wrong');
});
if (selectedCode === correctCode) {
quizState.score++;
}
setTimeout(() => {
quizState.currentQuestion++;
if (quizState.currentQuestion < quizState.totalQuestions) {
renderQuiz(quizState);
} else {
renderQuizResults(quizState);
}
}, 1500);
}
A natural extension of the gallery is an interactive tester that lets users make actual HTTP requests and see the responses in real time. The user enters a URL, selects an HTTP method, optionally adds headers and a body, and fires the request. The response status code is highlighted in the gallery, the full response headers are displayed, and the corresponding cat and dog images appear alongside the technical details. This transforms HTTP Zoo from a passive reference into an active debugging tool.
async function testHttpRequest(config) {
const { url, method, headers, body } = config;
const startTime = performance.now();
try {
const fetchOptions = {
method: method || 'GET',
headers: headers || {},
mode: 'cors'
};
if (body && ['POST', 'PUT', 'PATCH'].includes(method)) {
fetchOptions.body = body;
}
const response = await fetch(url, fetchOptions);
const endTime = performance.now();
// Parse response headers
const responseHeaders = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
// Try to read body
let responseBody = '';
const contentType = response.headers.get('content-type') || '';
try {
if (contentType.includes('json')) {
const json = await response.json();
responseBody = JSON.stringify(json, null, 2);
} else {
responseBody = await response.text();
if (responseBody.length > 5000) {
responseBody = responseBody.substring(0, 5000) + '\n... (truncated)';
}
}
} catch {
responseBody = '(Could not read response body)';
}
return {
success: true,
statusCode: response.status,
statusText: response.statusText,
headers: responseHeaders,
body: responseBody,
timing: Math.round(endTime - startTime),
redirected: response.redirected,
finalUrl: response.url,
type: response.type
};
} catch (err) {
return {
success: false,
error: err.message,
timing: Math.round(performance.now() - startTime),
statusCode: null,
hint: getErrorHint(err.message)
};
}
}
function getErrorHint(errorMessage) {
if (errorMessage.includes('CORS') || errorMessage.includes('cross-origin')) {
return 'The server does not allow cross-origin requests from the browser. ' +
'This is a CORS restriction, not an HTTP error. The request may work ' +
'from a server-side context or curl.';
}
if (errorMessage.includes('Failed to fetch') || errorMessage.includes('NetworkError')) {
return 'The request could not reach the server. Check the URL, your internet ' +
'connection, and whether the server is running.';
}
if (errorMessage.includes('TypeError')) {
return 'There may be a problem with the request configuration. Check the URL format ' +
'and ensure headers are valid.';
}
return 'An unexpected error occurred. Check the browser console for details.';
}
function renderRequestResult(result) {
const statusInfo = httpStatusCodes.find(c => c.code === result.statusCode);
return `
<div class="request-result ${result.success ? 'success' : 'error'}">
${result.success ? `
<div class="result-header">
<span class="status-badge" style="background: ${getStatusColor(result.statusCode)}">
${result.statusCode} ${result.statusText}
</span>
<span class="timing">${result.timing}ms</span>
</div>
${result.redirected ? `
<div class="redirect-notice">
Redirected to: ${result.finalUrl}
</div>
` : ''}
<div class="animal-preview">
<img src="https://http.cat/${result.statusCode}" alt="Cat"
style="max-height: 120px" />
<img src="https://http.dog/${result.statusCode}.jpg" alt="Dog"
style="max-height: 120px" />
</div>
${statusInfo ? `
<div class="status-explanation">
<p>${statusInfo.description}</p>
<p class="debug-tip">${statusInfo.debugTip}</p>
</div>
` : ''}
<details>
<summary>Response Headers (${Object.keys(result.headers).length})</summary>
<pre>${Object.entries(result.headers)
.map(([k, v]) => `${k}: ${v}`).join('\n')}</pre>
</details>
<details>
<summary>Response Body</summary>
<pre>${escapeHtml(result.body)}</pre>
</details>
` : `
<div class="error-result">
<p class="error-message">${result.error}</p>
<p class="error-hint">${result.hint}</p>
</div>
`}
</div>
`;
}
function getStatusColor(code) {
if (code < 200) return '#45aaf2';
if (code < 300) return '#26de81';
if (code < 400) return '#fed330';
if (code < 500) return '#ff6b6b';
return '#a55eea';
}
Beyond the quiz, a flashcard mode provides spaced-repetition learning for HTTP status codes. The user is shown either a code number and must recall the name and meaning, or shown a description and must identify the code. Correct answers increase the interval before the card is shown again; incorrect answers reset it. This is based on the Leitner system, a proven technique for memorization.
class FlashcardSystem {
constructor(codes) {
this.cards = codes.map(code => ({
code: code.code,
name: code.name,
description: code.description,
family: code.family,
box: 1, // Leitner box (1-5)
nextReview: 0, // Timestamp
attempts: 0,
correct: 0
}));
this.loadProgress();
}
getNextCard() {
const now = Date.now();
const dueCards = this.cards
.filter(c => c.nextReview <= now)
.sort((a, b) => a.box - b.box); // Prioritize lower boxes
return dueCards[0] || null;
}
markCorrect(code) {
const card = this.cards.find(c => c.code === code);
if (!card) return;
card.attempts++;
card.correct++;
card.box = Math.min(card.box + 1, 5);
// Interval increases exponentially with box number
const intervals = [0, 60000, 300000, 86400000, 604800000, 2592000000];
card.nextReview = Date.now() + intervals[card.box];
this.saveProgress();
}
markIncorrect(code) {
const card = this.cards.find(c => c.code === code);
if (!card) return;
card.attempts++;
card.box = 1; // Reset to box 1
card.nextReview = Date.now() + 30000; // Review again in 30 seconds
this.saveProgress();
}
getProgress() {
const total = this.cards.length;
const mastered = this.cards.filter(c => c.box >= 4).length;
const learning = this.cards.filter(c => c.box > 1 && c.box < 4).length;
const newCards = this.cards.filter(c => c.box === 1 && c.attempts === 0).length;
return {
total,
mastered,
learning,
newCards,
struggling: total - mastered - learning - newCards,
masteryPercent: ((mastered / total) * 100).toFixed(0)
};
}
saveProgress() {
try {
localStorage.setItem('httpzoo_flashcards', JSON.stringify(
this.cards.map(c => ({
code: c.code, box: c.box,
nextReview: c.nextReview,
attempts: c.attempts, correct: c.correct
}))
));
} catch {}
}
loadProgress() {
try {
const saved = JSON.parse(localStorage.getItem('httpzoo_flashcards'));
if (saved) {
saved.forEach(s => {
const card = this.cards.find(c => c.code === s.code);
if (card) Object.assign(card, s);
});
}
} catch {}
}
}
function renderFlashcard(card, showAnswer) {
return `
<div class="flashcard ${showAnswer ? 'flipped' : ''}">
<div class="flashcard-front">
<div class="card-code">${card.code}</div>
<p>What does this status code mean?</p>
<button onclick="revealAnswer()">Show Answer</button>
</div>
<div class="flashcard-back">
<div class="card-code">${card.code}</div>
<h3>${card.name}</h3>
<p>${card.description}</p>
<div class="flashcard-images">
<img src="https://http.cat/${card.code}" alt="Cat" />
<img src="https://http.dog/${card.code}.jpg" alt="Dog" />
</div>
<div class="flashcard-buttons">
<button class="btn-incorrect" onclick="answerIncorrect(${card.code})">
Got it Wrong
</button>
<button class="btn-correct" onclick="answerCorrect(${card.code})">
Got it Right
</button>
</div>
</div>
</div>
`;
}
For developers who encounter an error and need to diagnose it, we can build an interactive decision tree. The user answers a series of questions ("Did you authenticate?", "Is the resource supposed to exist?", "Did the server respond at all?"), and the tree narrows down to the most likely status code. This is essentially a diagnostic wizard for HTTP errors.
const decisionTree = {
question: 'Did the server respond at all?',
yes: {
question: 'Was the response successful (what you expected)?',
yes: {
question: 'Did the server create a new resource?',
yes: { code: 201, explanation: 'Resource created. Check Location header.' },
no: {
question: 'Is there a response body?',
yes: { code: 200, explanation: 'Standard success.' },
no: { code: 204, explanation: 'Success with no content to return.' }
}
},
no: {
question: 'Did the response redirect you?',
yes: {
question: 'Is the redirect permanent?',
yes: { code: 301, explanation: 'Update your bookmarks and links.' },
no: { code: 302, explanation: 'Temporary redirect. Original URL still valid.' }
},
no: {
question: 'Does the error seem to be your fault or the server\'s?',
client: {
question: 'Did you authenticate?',
no: { code: 401, explanation: 'Authentication required. Check your token.' },
yes: {
question: 'Do you have permission?',
no: { code: 403, explanation: 'Authenticated but not authorized.' },
yes: {
question: 'Does the resource exist?',
no: { code: 404, explanation: 'Resource not found at this URL.' },
yes: {
question: 'Did you send valid data?',
no: { code: 400, explanation: 'Check request body format and fields.' },
yes: {
question: 'Are you sending too many requests?',
yes: { code: 429, explanation: 'Rate limited. Implement backoff.' },
no: { code: 422, explanation: 'Valid syntax but semantic errors.' }
}
}
}
}
},
server: {
question: 'Is the server behind a proxy or load balancer?',
yes: {
question: 'Did the upstream server respond?',
no: { code: 502, explanation: 'Upstream server is down or returning invalid responses.' },
yes: { code: 504, explanation: 'Upstream server took too long to respond.' }
},
no: {
question: 'Is the server overloaded or under maintenance?',
yes: { code: 503, explanation: 'Temporary overload. Check Retry-After header.' },
no: { code: 500, explanation: 'Generic server error. Check server logs.' }
}
}
}
}
},
no: {
question: 'Did the connection time out?',
yes: { code: 408, explanation: 'Request timed out. Check network and server availability.' },
no: { code: null, explanation: 'Network error. Check your internet connection and the URL.' }
}
};
function renderDecisionNode(node, path = []) {
if (node.code !== undefined) {
// Leaf node - show the answer
const statusInfo = httpStatusCodes.find(c => c.code === node.code);
return `
<div class="decision-result">
<h3>Most likely: ${node.code} ${statusInfo?.name || ''}</h3>
<p>${node.explanation}</p>
<div class="result-images">
<img src="https://http.cat/${node.code}" alt="Cat" />
<img src="https://http.dog/${node.code}.jpg" alt="Dog" />
</div>
${statusInfo ? `<p class="debug-tip">${statusInfo.debugTip}</p>` : ''}
<button onclick="resetDecisionTree()">Start Over</button>
</div>
`;
}
// Question node
const options = Object.keys(node).filter(k => k !== 'question');
return `
<div class="decision-question">
<h3>${node.question}</h3>
<div class="decision-options">
${options.map(opt => `
<button onclick="advanceDecision('${opt}')"
class="decision-btn">
${opt.charAt(0).toUpperCase() + opt.slice(1)}
</button>
`).join('')}
</div>
</div>
`;
}
The decision tree encodes years of debugging experience into a simple interactive format. It handles the most common confusion points: the difference between 401 and 403 (authentication versus authorization), the difference between 502 and 504 (bad upstream response versus slow upstream response), and the distinction between 400 and 422 (syntax error versus semantic error). By walking through the tree, even junior developers can quickly narrow down the cause of an HTTP error.
A practical feature that developers will love is a custom error page generator. Select a status code, choose a style (professional, humorous, minimal, or branded), and HTTP Zoo generates a complete HTML error page that can be dropped directly into any web project. The page includes the appropriate animal images, a user-friendly error message, and technical details for developers who view the page source.
function generateErrorPage(code, style = 'professional') {
const statusInfo = httpStatusCodes.find(c => c.code === code);
if (!statusInfo) return null;
const userMessages = {
400: 'Something went wrong with your request. Please check and try again.',
401: 'You need to log in to access this page.',
403: 'You do not have permission to view this page.',
404: 'The page you are looking for does not exist or has been moved.',
405: 'This action is not supported.',
408: 'The request took too long. Please try again.',
429: 'Too many requests. Please slow down and try again shortly.',
500: 'Something went wrong on our end. We are working to fix it.',
502: 'We are having trouble reaching our servers. Please try again.',
503: 'We are temporarily down for maintenance. We will be back soon.',
504: 'Our servers are taking too long to respond. Please try again.'
};
const styles = {
professional: {
bg: '#f8f9fa', color: '#333', accent: '#0066cc',
font: '-apple-system, sans-serif'
},
humorous: {
bg: '#1a1a2e', color: '#e0e0e8', accent: '#ff6b6b',
font: '-apple-system, sans-serif'
},
minimal: {
bg: '#ffffff', color: '#111', accent: '#111',
font: 'Georgia, serif'
}
};
const s = styles[style] || styles.professional;
const userMsg = userMessages[code] || statusInfo.description;
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${code} - ${statusInfo.name}</title>
<style>
body { font-family: ${s.font}; background: ${s.bg}; color: ${s.color};
display: flex; align-items: center; justify-content: center;
min-height: 100vh; margin: 0; text-align: center; }
.container { max-width: 600px; padding: 2rem; }
h1 { font-size: 4rem; color: ${s.accent}; margin-bottom: 0.5rem; }
h2 { font-size: 1.5rem; margin-bottom: 1.5rem; font-weight: 400; }
p { line-height: 1.8; margin-bottom: 1.5rem; }
.animal { max-width: 300px; border-radius: 12px; margin: 1.5rem auto;
display: block; }
a { color: ${s.accent}; text-decoration: none; }
a:hover { text-decoration: underline; }
.tech-details { font-size: 0.8rem; color: ${s.color}88; margin-top: 3rem; }
</style>
</head>
<body>
<div class="container">
<h1>${code}</h1>
<h2>${statusInfo.name}</h2>
<img src="https://http.cat/${code}" alt="${statusInfo.name}"
class="animal" onerror="this.style.display='none'" />
<p>${userMsg}</p>
<p><a href="/">Go back to the homepage</a></p>
<!-- Developer info -->
<div class="tech-details">
<p>HTTP ${code} ${statusInfo.name} | ${statusInfo.rfcSection}</p>
<p>Debug: ${statusInfo.debugTip}</p>
</div>
</div>
</body>
</html>`;
}
Not every HTTP status code has an image in the HTTP Cat and HTTP Dog databases. Codes like 102 Processing, 103 Early Hints, and 431 Request Header Fields Too Large may not have images. Our error handler replaces missing images with a styled placeholder, but the inconsistency can make the gallery look uneven. Consider creating your own placeholder images with consistent dimensions and styling.
With dozens of images, initial page load can be slow on weak connections. The lazy loading strategy helps, but even lazy-loaded images create HTTP requests. For production, consider generating a sprite sheet of thumbnails (small preview images) that loads as a single request, and only loading the full-resolution images when a card is expanded or focused.
Status code 418 was defined in RFC 2324 (Hyper Text Coffee Pot Control Protocol) as an April Fools' joke in 1998. For years, there was debate about whether it should be removed from common implementations. In 2017, Mark Nottingham (then chair of the HTTP Working Group) proposed deprecating it, sparking a surprisingly passionate community response. The code survived and remains a beloved part of HTTP culture. Our gallery includes it with a special styling treatment to honor its unique heritage.
While displaying images works fine cross-origin, if you want to implement a "download" or "share" feature that involves drawing images to a canvas, you will hit CORS restrictions. Neither HTTP Cat nor HTTP Dog images include Access-Control-Allow-Origin headers that allow canvas operations. You would need a server-side proxy to download the image and re-serve it with CORS headers if you need canvas access.
New developers joining a team can use HTTP Zoo as a friendly introduction to the HTTP codes they will encounter in logs, API responses, and monitoring dashboards. The visual associations (a confused cat for 404, a tired dog for 503) create memorable mental anchors that text-only references cannot match.
API documentation can embed HTTP Zoo links for each status code their API returns. Instead of a dry table of codes, users can click through to see the animal image, a clear explanation, and debugging tips. This makes error handling documentation more approachable and more likely to be actually read.
Error monitoring dashboards (like Grafana, Datadog, or custom dashboards) can use the animal images as visual indicators for error rates. A dashboard widget that shows the HTTP Cat for 500 when server errors spike adds a touch of levity to what can be a stressful debugging session.
A browser extension that detects HTTP errors and shows the corresponding animal image in a notification would be both useful and delightful. Encounter a 403 while browsing? A cat appears to tell you that you do not have permission. This would be particularly valuable for web developers who spend their days navigating between working and broken pages.
Computer science instructors teaching web development or networking courses can use HTTP Zoo as an interactive teaching aid. The quiz mode makes it easy to test student knowledge, and the visual format keeps students engaged during what might otherwise be a dry lecture about protocol specifications.
One of the most elegant aspects of this project is that it requires zero JavaScript API calls. The images are loaded directly by the browser via img src attributes. The status code data is a static JavaScript object defined in the source code. This means the application works offline (except for the images), has no CORS issues, and loads almost instantly. This is a reminder that not every project needs fetch calls and async/await patterns. Sometimes the simplest approach is the most performant.
The CSS content-visibility: auto property tells the browser to skip rendering for elements that are off-screen. For a gallery with 50+ cards, this significantly reduces initial rendering time because the browser only needs to lay out and paint the cards currently in the viewport.
.status-card {
content-visibility: auto;
contain-intrinsic-size: 0 400px; /* Estimated height */
}
Since the status code data is static, we can cache everything in a service worker for offline access. The service worker caches the HTML, CSS, JavaScript, and (progressively) the animal images. After the first visit, the entire gallery works offline, making it a true reference tool that developers can use anywhere.
If you host your own image mirror (for faster loading or offline access), convert the JPEG images to WebP format. WebP typically offers 25-30% smaller file sizes at equivalent quality, which adds up across 100+ images. Use the picture element with a JPEG fallback for browsers that do not support WebP (though this is increasingly rare).
Build a visual timeline showing when each status code was introduced, which RFC defined it, and who authored the RFC. This adds a historical dimension to the gallery and helps developers understand how the HTTP specification has evolved from HTTP/0.9 in 1991 to HTTP/3 today.
Add a feature that lets users make actual HTTP requests to test endpoints and see the response status code in real time, complete with the corresponding animal image. This transforms the gallery from a passive reference into an active debugging tool.
Allow users to vote on which animal image better represents each status code. Over time, build a community consensus on the definitive cat vs. dog ranking for each code. This adds a social, gamified dimension that would drive repeat visits and sharing.
Generate downloadable custom error page HTML for any status code, pre-styled with the animal images and a clear user-friendly message. Web developers could drop these directly into their projects for instant, delightful error pages.
Build a companion tool where users paste a full HTTP response (headers and body) and the tool highlights the status code, parses the headers, and provides context-specific advice based on the combination of status code, headers, and body content. This would be a Swiss Army knife for HTTP debugging.
One of the most practical features we can add is a downloadable cheat sheet that summarizes the most important status codes in a printable format. Developers can pin it next to their monitors or include it in team onboarding materials. The generator creates a clean, print-friendly HTML page that uses CSS print media queries for proper formatting.
function generateCheatSheet(codes, options = {}) {
const {
includeImages = false,
groupByFamily = true,
includeDebugTips = true,
maxCodes = 30
} = options;
// Select the most important codes
const priorityCodes = [
100, 101, 200, 201, 204, 206, 301, 302, 304, 307,
400, 401, 403, 404, 405, 408, 409, 418, 422, 429,
500, 502, 503, 504
];
const selectedCodes = codes
.filter(c => priorityCodes.includes(c.code))
.sort((a, b) => a.code - b.code)
.slice(0, maxCodes);
const familyColors = {
'1xx': '#45aaf2',
'2xx': '#26de81',
'3xx': '#fed330',
'4xx': '#ff6b6b',
'5xx': '#a55eea'
};
const familyDescriptions = {
'1xx': 'Informational - Request received, processing continues',
'2xx': 'Success - Request was received, understood, and accepted',
'3xx': 'Redirection - Further action needed to complete the request',
'4xx': 'Client Error - The request contains bad syntax or cannot be fulfilled',
'5xx': 'Server Error - The server failed to fulfill a valid request'
};
let html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>HTTP Status Code Cheat Sheet</title>
<style>
@media print {
body { font-size: 9pt; }
.no-print { display: none; }
.family-section { page-break-inside: avoid; }
}
body { font-family: -apple-system, sans-serif; max-width: 900px;
margin: 0 auto; padding: 1rem; color: #333; }
h1 { text-align: center; margin-bottom: 0.5rem; }
.subtitle { text-align: center; color: #666; margin-bottom: 2rem; }
.family-header { padding: 0.5rem 1rem; color: white; margin: 1.5rem 0 0.5rem;
border-radius: 6px; }
.family-desc { font-size: 0.85rem; color: #666; margin-bottom: 0.5rem; }
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
th { text-align: left; padding: 0.4rem; border-bottom: 2px solid #ddd; }
td { padding: 0.4rem; border-bottom: 1px solid #eee; vertical-align: top; }
.code { font-weight: bold; font-family: monospace; font-size: 1rem; }
.debug { font-size: 0.8rem; color: #888; font-style: italic; }
.print-btn { display: block; margin: 1rem auto; padding: 0.75rem 2rem;
background: #0066cc; color: white; border: none; border-radius: 6px;
cursor: pointer; font-size: 1rem; }
</style>
</head>
<body>
<h1>HTTP Status Code Cheat Sheet</h1>
<p class="subtitle">The essential reference for web developers</p>
<button class="print-btn no-print" onclick="window.print()">Print This Page</button>`;
if (groupByFamily) {
const grouped = {};
selectedCodes.forEach(code => {
if (!grouped[code.family]) grouped[code.family] = [];
grouped[code.family].push(code);
});
for (const [family, familyCodes] of Object.entries(grouped)) {
html += `
<div class="family-section">
<div class="family-header" style="background: ${familyColors[family]}">
${family} ${familyCodes[0].familyName}
</div>
<p class="family-desc">${familyDescriptions[family]}</p>
<table>
<tr><th width="80">Code</th><th width="150">Name</th>
<th>Description</th>
${includeDebugTips ? '<th width="200">Debug Tip</th>' : ''}
</tr>
${familyCodes.map(code => `
<tr>
<td class="code">${code.code}</td>
<td>${code.name}</td>
<td>${code.description}</td>
${includeDebugTips ? `<td class="debug">${code.debugTip}</td>` : ''}
</tr>
`).join('')}
</table>
</div>`;
}
}
html += `
<p style="text-align:center; color:#999; margin-top:2rem; font-size:0.8rem">
Generated by HTTP Zoo | httpzoo.dev
</p>
</body>
</html>`;
return html;
}
function downloadCheatSheet() {
const html = generateCheatSheet(httpStatusCodes);
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'http-status-codes-cheat-sheet.html';
link.click();
URL.revokeObjectURL(url);
}
The cheat sheet generator is simple but thoughtful. It uses print-friendly CSS so the output looks good both on screen and on paper. The debug tips column can be toggled for a more compact view. The family color coding provides visual grouping that helps with quick scanning. And the entire thing is a single self-contained HTML file that requires no external dependencies, making it easy to share, host, or print.
For developers who want to integrate HTTP Zoo's data into their own tools, we can expose the status code database as a clean JavaScript module. This module provides lookup by code, filtering by family, random code selection (useful for quizzes), and statistics about the code distribution.
// httpzoo.js - Reusable status code module
const HTTPZoo = {
codes: httpStatusCodes,
lookup(code) {
return this.codes.find(c => c.code === code) || null;
},
family(familyCode) {
return this.codes.filter(c => c.family === familyCode);
},
random(options = {}) {
const { family, exclude = [] } = options;
let pool = this.codes;
if (family) pool = pool.filter(c => c.family === family);
pool = pool.filter(c => !exclude.includes(c.code));
return pool[Math.floor(Math.random() * pool.length)];
},
search(query) {
const q = query.toLowerCase();
return this.codes.filter(c =>
String(c.code).includes(q) ||
c.name.toLowerCase().includes(q) ||
c.description.toLowerCase().includes(q)
);
},
catUrl(code) {
return `https://http.cat/${code}`;
},
dogUrl(code) {
return `https://http.dog/${code}.jpg`;
},
isSuccess(code) { return code >= 200 && code < 300; },
isRedirect(code) { return code >= 300 && code < 400; },
isClientError(code) { return code >= 400 && code < 500; },
isServerError(code) { return code >= 500 && code < 600; },
stats() {
const families = {};
this.codes.forEach(c => {
families[c.family] = (families[c.family] || 0) + 1;
});
return {
total: this.codes.length,
byFamily: families,
hasImage: this.codes.length // Approximate
};
}
};
// Usage examples:
// HTTPZoo.lookup(404)
// HTTPZoo.family('4xx')
// HTTPZoo.random({ family: '5xx' })
// HTTPZoo.catUrl(418)
// HTTPZoo.isClientError(403) // true
Building HTTP Zoo gives you a deeper appreciation for the thoughtfulness of the HTTP specification. The five-family system is not arbitrary; it reflects a clear conceptual model of what can happen when a client makes a request. The server can say "I'm working on it" (1xx), "Here you go" (2xx), "Look over there" (3xx), "You messed up" (4xx), or "I messed up" (5xx). This simplicity is what makes HTTP so resilient and widely adopted.
The specific codes within each family reveal the protocol designers' attention to real-world needs. The existence of 206 Partial Content shows they anticipated streaming media. The existence of 429 Too Many Requests (added in 2012, long after the original HTTP/1.1 spec) shows the protocol can evolve to address new challenges like API rate limiting. The existence of 418 I'm a Teapot shows they have a sense of humor.
Understanding HTTP status codes deeply makes you a better developer. When you see a 502 in your logs, you immediately know the problem is not in your server but in the upstream service it depends on. When you see a 304, you know your caching strategy is working. When you see a 307 instead of a 302, you know someone cared about preserving the request method during the redirect. These distinctions matter, and HTTP Zoo makes them stick by associating them with memorable images and clear explanations.
There is also a historical dimension worth appreciating. The original HTTP/1.0 specification, defined in RFC 1945 in 1996, included only a handful of status codes. HTTP/1.1 (RFC 2616, 1999) expanded the list significantly, adding codes like 409 Conflict, 410 Gone, and 415 Unsupported Media Type that reflected the growing complexity of web applications. Since then, new codes have been added through individual RFCs as the web evolved: 422 Unprocessable Entity came from WebDAV, 429 Too Many Requests addressed the rise of API rate limiting, and 451 Unavailable For Legal Reasons (a reference to Ray Bradbury's Fahrenheit 451) was added in 2015 to handle government censorship and legal takedowns. Each new code tells a story about how the web changed and what new problems developers needed to communicate about.
HTTP/2 and HTTP/3 did not introduce new status codes because the semantics layer and the transport layer are deliberately separated. Status codes describe what happened to the request at the application level, regardless of whether the bytes traveled over TCP, TLS, QUIC, or carrier pigeon. This separation of concerns is one of the great architectural decisions in the history of computing, and it means that HTTP Zoo remains accurate and relevant across all versions of the protocol. A 404 means the same thing whether the response arrives via HTTP/1.1 over plain TCP or HTTP/3 over QUIC with zero-round-trip connection resumption. The transport changes, but the semantics endure.
Building HTTP Zoo also surfaces an interesting observation about API design philosophy. The HTTP Cat and HTTP Dog APIs are examples of what you might call "decorative APIs" — they do not provide data, they provide context. Their sole purpose is to make another system (the HTTP protocol) more approachable and memorable. This is a legitimate and undervalued category of API. Most API directories focus on data APIs (weather, stocks, news) or service APIs (email, payments, authentication). But decorative APIs — those that add personality, humor, or visual richness to other systems — serve a genuine need in developer experience. The fact that HTTP Cat and HTTP Dog have been running for years and remain popular proves that developers value not just functionality but also joy in their tools.
HTTP Zoo proves that developer education does not have to be boring. By combining HTTP Cat and HTTP Dog into an interactive gallery with search, filtering, a quiz mode, and detailed explanations, we have built a tool that is simultaneously entertaining and genuinely useful. The cute animal images create emotional associations that make the status codes memorable, while the technical details ensure that the gallery serves as a legitimate reference.
The technical simplicity of the project is a feature, not a limitation. With no API calls, no backend, no build step, and no framework, the entire application loads in milliseconds and works offline with a service worker. This is a powerful demonstration that the best developer tools are often the simplest. Not every project needs React, a GraphQL API, and a Kubernetes cluster. Sometimes you just need a well-organized HTML page with thoughtful CSS and a bit of JavaScript.
The patterns we explored, including lazy loading, Intersection Observer-based preloading, hash-based deep linking, content visibility optimization, and gamified learning through quizzes, are applicable to any content-heavy web application. Whether you are building a documentation site, a product gallery, or an educational platform, these techniques will make your application faster, more engaging, and more useful.
Bookmark HTTP Zoo. Share it with your team. Use it the next time you encounter a mysterious status code in your logs. And remember: behind every 500 Internal Server Error, there is a very panicked cat.