signalapp/Signal-Server · warning · ClientErrorException
?…
Error message
? Response.status(418).entity(verificationSessionResponse).build()
What it means
requestVerificationCode returns HTTP 418 ("I'm a teapot", Signal's convention for transport-not-allowed) when the registration service rejects the requested verification transport (e.g. voice call or SMS) for this session/number. If the exception carries a registration session, the client gets a 418 (TransportNotAllowedException) or 409 (other RegistrationServiceException) with an updated VerificationSessionResponse body; otherwise it gets 404.
Solutions
- Switch to a different transport (e.g. request a voice call instead of SMS) for this session
- Use the returned verificationSessionResponse to continue the existing session rather than starting a new request
- Wait and retry later if the transport is temporarily blocked by fraud rules
- Check registration-service configuration/allow-lists if a region should permit the transport
Example fix
// before: retry same transport forever
requestCode(phoneNumber, Transport.SMS);
// after
if (response.code() == 418) {
updateSessionFromResponse(response.body());
requestCode(phoneNumber, Transport.VOICE); // try alternate transport
} Defensive patterns
Strategy: try-catch
Validate before calling
// check whether the session allows the requested transport first
if (!session.getAllowedTransports().contains(requestedTransport)) {
requestedTransport = session.getAllowedTransports().iterator().next();
} Try / catch
try { requestVerificationCode(session, transport); }
catch (ClientErrorException e) {
if (e.getResponse().getStatus() == 418) {
updateSession(e.getResponse().readEntity(VerificationSessionResponse.class));
switchTransportAndRetry();
}
} Prevention
- Advertise/surface allowed transports from the session state
- Never retry the same disallowed transport in a loop
- Resume the existing session using the returned session response
- Watch fraud-rule feedback (418) and back off rather than escalating
When it happens
Trigger: POST/PUT to request a verification code where the registration service refuses the chosen channel — e.g. requesting SMS for a number flagged to only allow voice, or a transport blocked by fraud detection for that session.
Common situations: Numbers in regions where SMS delivery is disabled/blocked; carriers/routes flagged by fraud prevention; clients retrying the same disallowed transport in a loop instead of switching channel or completing an existing session.
Related errors
- registration session is unverified
- recovery password could not be verified
- return Response.status(508).build();
- number does not match session
- registration session is unverified
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/1af7a305de093b89.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java:649
final RegistrationServiceSession resultSession;
try {
resultSession = registrationServiceClient.sendVerificationCode(registrationServiceSession.id(),
messageTransport,
clientType,
acceptLanguage.orElse(null),
senderOverride,
REGISTRATION_RPC_TIMEOUT);
} catch (final VerificationSessionRateLimitExceededException e) {
throw new ClientErrorException(buildResponseForRateLimitExceeded(verificationSession,
e.getRegistrationSession(),
e.getRetryDuration()));
} catch (final RegistrationServiceException registrationServiceException) {
throw registrationServiceException.getRegistrationSession()
.map(s -> buildResponse(s, verificationSession))
.map(verificationSessionResponse -> {
final Response response = registrationServiceException instanceof TransportNotAllowedException
? Response.status(418).entity(verificationSessionResponse).build()
: Response.status(Response.Status.CONFLICT).entity(verificationSessionResponse).build();
return new ClientErrorException(response);
})
.orElseGet(NotFoundException::new);
} catch (final RegistrationFraudException e) {
if (dynamicConfigurationManager.getConfiguration().getRegistrationConfiguration()
.squashDeclinedAttemptErrors()) {
return buildResponse(registrationServiceSession, verificationSession);
} else {
throw e.getCause();
}
} catch (final RuntimeException e) {
logger.error("Registration service failure", e);
throw new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR);
}
accountsManager.getByE164(registrationServiceSession.number())View on GitHub (pinned to 100ab61c82)