Web routes: an integration calling in
A Serverpod route that answers an outside system — a payment webhook, a delivery callback, a partner's push — is an HTTP handler, not an endpoint. It has no session of ours, no authenticated user, and no client library on the other side. What it needs is always the same four things: read the body once, log the request, decide what the caller is allowed to see when it fails, and answer in a shape they can parse.
The framework owns all four. DwWebServerLog is a framework table and arrives
by migration in every project, so "integration requests are logged" is settled
upstream; DwWebServerLogger is what writes into it.
Future<bool> handleCall(Session session, HttpRequest request) =>
DwWebServerLogger.handleWithExceptions(
session,
request,
handler: 'PaymentCallback',
action: (body) async {
final payload = jsonDecode(body ?? '{}') as Map<String, dynamic>;
final orderId = payload['orderId'];
if (orderId == null) {
throw const DwPublicWebException('orderId is required');
}
await _apply(session, orderId as int);
return {'applied': true};
},
);
A successful action becomes {"success": true, "data": …} with 200.
What the caller is told when it fails
Only a DwPublicWebException reaches them. Its message is the response
body and its statusCode the status — 400 unless you say otherwise.
Anything else is answered with 500 and a fixed sentence. That is not caution
for its own sake: an arbitrary exception carries whatever it carries — a
database error carries its query, a null check carries a file path, a framework
failure carries whatever the framework felt like saying — and it was written
for us, not for a caller we never authenticated. The text is not lost; it goes
to the alert and into the DwWebServerLog row, where it is of use.
So "safe to show the caller" is a type, not a convention:
throw const DwPublicWebException('unknown signature', statusCode: 401);
A public failure raises no alert. The route refusing on its own terms is not an incident, and alerting on every malformed request is how people learn to stop reading alerts. It is still recorded in the log row.
What reaches the log table
Headers and the JSON body are written with sensitive values replaced, walking maps and lists — a payload keeps its secrets one level inside an array as readily as under a key. A body that is not JSON is not logged at all.
DwWebServerLogger.knownSensitiveKeys is the maintained list (authorization,
cookie, x-api-key, token, secret, …). Pass sensitiveKeys: to extend it
for a route whose partner names things its own way — and add it to the framework
list instead when the name is not project-specific, because the next project
will meet the same partner.
The log row records the method, URL, sanitised headers and body, status, status code, error text, duration, handler name and caller IP. Writing it never fails the request: a failure to log is alerted and swallowed.