AI Prompt Engineering

Getting ChatGPT to Write Accurate Logging Middleware Without Swallowing Errors

June 28, 2026 9 min read 2 views

You ask ChatGPT for logging middleware, it spits out something that looks clean, and you ship it. Two weeks later you realize your error logs are empty because the middleware wrapped every exception in a bare try/except and logged a generic message instead of re-raising. That's not logging β€” it's a silence machine.

This pattern shows up constantly in AI-generated middleware. The fix isn't to avoid ChatGPT; it's to prompt it precisely enough that it can't take the easy swallow-and-move-on path.

What You'll Learn

  • Why ChatGPT tends to generate error-swallowing middleware by default
  • How to write prompts that enforce error propagation and structured logging
  • Concrete middleware examples in Python (WSGI/ASGI) and Node.js/Express
  • What a well-structured log entry must include to be useful in production
  • Common pitfalls to catch before the code reaches your repo

Prerequisites

You should be comfortable reading middleware code in at least one of Python or JavaScript. Familiarity with structured logging concepts (JSON log entries, correlation IDs) will help, but the article explains them as they come up. No specific framework version is assumed.

The Core Problem With AI-Generated Logging Middleware

When you ask for "logging middleware," ChatGPT optimizes for the most common tutorial pattern it has seen: wrap the request in a try/except, log something on failure, return a clean response. That pattern prioritizes user-facing stability over operational visibility. In a production system, you need both β€” but the logging must never become the reason an error disappears.

The canonical bad output looks like this:

async def logging_middleware(request, call_next):
    try:
        response = await call_next(request)
        logger.info(f"{request.method} {request.url} - {response.status_code}")
        return response
    except Exception as e:
        logger.error(f"Request failed: {e}")
        return JSONResponse(status_code=500, content={"error": "Internal server error"})

This catches the exception, logs a one-line message, and returns a 500. The original exception β€” its type, traceback, and all the context downstream error handlers or Sentry might have used β€” is gone. Any error handler you registered below this middleware never runs.

Why ChatGPT Defaults to Swallowing Errors

Training data for web frameworks is full of examples that prioritize clean HTTP responses over observability. Stack Overflow answers, blog posts, and quick-start guides almost always show a bare except block that returns a friendly error page. ChatGPT has seen far fewer examples of the correct pattern: log the error and then re-raise it, or log it without catching it at all.

There's also an ambiguity problem. The phrase "handle errors" is overloaded. To ChatGPT it can mean "catch them and return a 500," which is a valid interpretation in some contexts. You need to make your intent explicit in the prompt.

This is the same class of subtle omission that appears in rate limiting middleware generated by ChatGPT β€” the model produces something that looks correct until you test an edge case it never considered.

How to Frame Your Prompt Correctly

The most effective change you can make is to describe your constraints, not just your goal. Instead of "write logging middleware for FastAPI," give the model a set of non-negotiable rules.

Here's a prompt template that consistently produces better output:

Write FastAPI middleware that logs every HTTP request and response.

Rules:
1. Log the method, path, status code, and duration in milliseconds as a JSON object.
2. If an exception is raised during request processing, log the full traceback using
   `traceback.format_exc()` and then RE-RAISE the exception β€” do NOT swallow it.
3. Never return a response from inside an except block. Let the exception propagate
   to FastAPI's own exception handlers.
4. Include a correlation ID (from the X-Request-ID header, or generate a UUID if absent)
   in every log entry.
5. Use Python's standard `logging` module, not `print`.

Do not add any code not explicitly asked for. Show the middleware class only.

Notice what the prompt does: it names the exact logging call to use (traceback.format_exc()), explicitly forbids the swallow pattern, and separates the logging concern from the error-response concern. Giving ChatGPT a "do not" rule is often more effective than a "do" rule, because it removes the shortcut the model would otherwise take.

Building a Python Logging Middleware With Full Error Context

Here is the output you should expect from a well-formed prompt, cleaned up and annotated. This targets FastAPI, but the pattern transfers to any ASGI framework.

import logging
import time
import traceback
import uuid
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response

logger = logging.getLogger(__name__)

class RequestLoggingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next) -> Response:
        correlation_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
        start = time.perf_counter()

        log_context = {
            "correlation_id": correlation_id,
            "method": request.method,
            "path": str(request.url.path),
            "query": str(request.url.query),
        }

        try:
            response = await call_next(request)
            log_context["status_code"] = response.status_code
            log_context["duration_ms"] = round((time.perf_counter() - start) * 1000, 2)
            logger.info("request completed", extra={"data": log_context})
            return response
        except Exception:
            log_context["duration_ms"] = round((time.perf_counter() - start) * 1000, 2)
            log_context["traceback"] = traceback.format_exc()
            logger.error("unhandled exception during request", extra={"data": log_context})
            raise  # Always re-raise β€” never swallow

The critical line is the bare raise at the bottom. It re-raises the original exception with its full traceback intact, so any exception handler registered with FastAPI (including Sentry's SDK, your custom @app.exception_handler, or Starlette's default 500 handler) still runs. The middleware's job is observation, not interception.

Note also the use of except Exception: without capturing the variable. Using traceback.format_exc() retrieves the current exception's traceback from the interpreter's context, so you don't need to reference e at all β€” which avoids accidentally holding a reference that could delay garbage collection in long-running processes.

Building a Node.js Express Logging Middleware That Preserves Errors

Express uses a four-argument function signature for error middleware: (err, req, res, next). A common AI-generated mistake is mixing logging middleware with error-handling middleware into a single function, which causes Express to route all requests through the error path regardless of whether an error occurred.

Prompt rule to add: "Keep request logging middleware and error logging middleware as two separate functions. The request logger must call next() unconditionally. The error logger must call next(err) after logging so the default error handler still runs."

const { v4: uuidv4 } = require('uuid');

// Middleware 1: log every request/response
function requestLogger(req, res, next) {
  const correlationId = req.headers['x-request-id'] || uuidv4();
  const start = Date.now();

  res.on('finish', () => {
    const entry = {
      correlation_id: correlationId,
      method: req.method,
      path: req.path,
      status_code: res.statusCode,
      duration_ms: Date.now() - start,
    };
    console.log(JSON.stringify(entry));
  });

  next();
}

// Middleware 2: log errors, then pass them on β€” NEVER swallow
function errorLogger(err, req, res, next) {
  const entry = {
    correlation_id: req.headers['x-request-id'] || 'unknown',
    method: req.method,
    path: req.path,
    error_message: err.message,
    stack: err.stack,
  };
  console.error(JSON.stringify(entry));
  next(err); // Hand off to Express's default error handler or your own
}

module.exports = { requestLogger, errorLogger };

Mount them in the right order: requestLogger near the top of your middleware stack, errorLogger after your routes but before any final error-response middleware. This mirrors the ordering discipline required for async middleware β€” sequence matters more than most AI-generated examples show.

Structured Logging: What to Include in Every Log Entry

ChatGPT will often generate logging that writes human-readable strings. That's fine for development, but production log aggregators (Datadog, Loki, CloudWatch Logs Insights) work best with JSON. When you prompt for logging middleware, explicitly require structured output.

A minimal structured log entry for a request should contain:

  • correlation_id β€” ties a request across multiple services and log lines
  • method β€” HTTP verb
  • path β€” URL path without query string (to avoid PII in log keys)
  • status_code β€” response code, captured on finish not on entry
  • duration_ms β€” wall-clock time from request start to response flush
  • error / stack β€” only present when an exception occurred

Query strings, request bodies, and authorization headers should be explicitly excluded or redacted. Tell ChatGPT this in the prompt, or it will often log the full URL including tokens as query parameters. Add a rule like: "Never log the Authorization header or any query parameter whose key contains 'token', 'key', or 'secret'."

Common Pitfalls in AI-Generated Logging Code

Catching BaseException instead of Exception

Occasionally ChatGPT will generate except BaseException, which catches KeyboardInterrupt and SystemExit. Those signals are how your process manager stops the server gracefully. Catching them in middleware and re-raising still adds latency to shutdown and can interfere with signal handlers. Stick to except Exception.

Logging inside the response after streaming has started

In ASGI frameworks, the response body may already be streaming by the time your middleware's try block exits. Logging the status code before response.body_iterator is consumed gives you the wrong code for streaming responses. The Node.js res.on('finish') pattern avoids this; in Python, use a background task or consume the iterator before logging.

Duplicate log entries from nested middleware

If you're using a framework that already logs requests (Django's django.request logger, for example), adding a second middleware creates duplicate entries. Prompt ChatGPT with the specific framework version and mention any logging that's already in place so it can account for this. This mirrors the configuration drift problem described in environment variable config generation β€” context the model doesn't have leads to redundant or conflicting output.

Missing context on 4xx errors

A bare 404 or 422 validation error often never reaches an except block because the framework returns the error response directly without raising. If you want those logged with the same structure as 5xx errors, you need to inspect response.status_code in the success path and emit a warning log for codes above 399. Tell ChatGPT this explicitly, or the 4xx path will be silent.

Performance cost of logging every request body

ChatGPT sometimes adds request body logging "for completeness." Reading the body in middleware consumes it, meaning your route handler receives an empty body unless you buffer and re-inject it β€” which adds memory overhead on large payloads. Unless you specifically need body logging, exclude it from the prompt.

These production-critical edge cases are exactly the kind of thing that also comes up in webhook handler generation β€” the model's first draft handles the happy path well and leaves the edges under-specified.

Wrapping Up

The problem with AI-generated logging middleware isn't that the model doesn't know how logging works. It's that the default prompt gives it room to take shortcuts that look harmless until you're debugging an outage with no stack traces. Here are the concrete steps to take right now:

  1. Add explicit re-raise rules to every middleware prompt. State "re-raise the exception after logging" and "never return from inside an except block" as hard constraints.
  2. Require structured JSON output. Specify every field you need and every field that must be excluded (tokens, secrets, raw bodies).
  3. Separate logging middleware from error-response middleware. They have different jobs; a single function shouldn't do both.
  4. Test with a deliberate exception. After integrating any AI-generated middleware, throw a test exception and verify the traceback appears in your log aggregator and reaches your error tracker.
  5. Review the generated code for bare except blocks before merging. A bare except with no re-raise is almost always wrong in middleware.

Frequently Asked Questions

Why does ChatGPT generate logging middleware that swallows exceptions instead of re-raising them?

ChatGPT's training data contains many tutorial examples that prioritize clean HTTP responses over observability, so it defaults to catching and returning a 500 rather than re-raising. You can override this by explicitly including a rule in your prompt that says to re-raise the exception after logging.

How do I make sure my logging middleware doesn't interfere with my existing error handlers?

Always use a bare `raise` (Python) or `next(err)` (Express) after logging an exception, so the error propagates to any downstream handlers like Sentry, custom exception handlers, or the framework's default 500 handler. Never return a response from inside the except block of a logging middleware.

What fields should every structured log entry include for production HTTP middleware?

At minimum include a correlation ID, HTTP method, URL path, response status code, and request duration in milliseconds. Exclude the Authorization header, query parameters containing tokens or keys, and raw request bodies unless you have a specific requirement for them.

Can I use the same middleware function for both logging requests and sending error responses?

It's better to keep them separate. A logging middleware should only observe and then call next, while an error-response middleware should format and send the error reply. Mixing them makes it harder to reason about which handler runs and in what order.

How do I test that my logging middleware isn't silently swallowing exceptions?

Deliberately throw an unhandled exception in a test route, then verify the full traceback appears in your log output and that the exception reaches your error tracker like Sentry. If your log shows a message but Sentry shows nothing, the exception is being swallowed somewhere in the middleware chain.

πŸ“€ Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

πŸ“¬ Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.