Reference — Errors
HTTP error catalog, pending codes, LolApiError shape and LolApiExceptionHandler mapping.
Shape — LolApiError
LolApiError (src/main/java/com/safjnest/spring/dto/LolApiError.java:5) — sole Spring DTO for errors (ADR-0005/0007):
public record LolApiError(int status, String code, String message, ResponseMetadata metadata) {
public LolApiError(int status, String code, String message) { this(status, code, message, null); }
}
ResponseMetadata (src/main/java/com/safjnest/lol/model/ResponseMetadata.java:3): Pagination pagination, Long lastUpdate, Boolean refresh, Filter filter — optional, populated only for 202 PENDING with context.
{
"status": 202,
"code": "profile_pending",
"message": "Profile initialization is pending",
"metadata": {
"pagination": null,
"lastUpdate": null,
"refresh": true,
"filter": null
}
}
The second constructor without metadata is used for 400/404/405 from LolApiExceptionHandler.
HTTP catalog
| HTTP | code | Example message | Source | File:line |
|---|---|---|---|---|
400 | invalid_request | Invalid rank: must be one of: IRON, BRONZE, ... | LolApiParameters.invalid | LolApiParameters.java:235 |
400 | invalid_request | Missing required query parameter 'q' | MissingServletRequestParameterException | LolApiExceptionHandler.java:31 |
400 | invalid_request | Missing required path parameter 'puuid' | MissingPathVariableException | :40 |
400 | invalid_request | Invalid value for parameter 'page' | MethodArgumentTypeMismatchException | :49 |
400 | invalid_request | Invalid shard: is required | requiredShard | LolApiParameters.java:163 |
400 | invalid_request | Invalid patch: must have the form major.minor | patch | :103 |
400 | invalid_request | Invalid limit: must be between 1 and 50 | limit | :203 |
400 | invalid_request | Invalid sort: must be timeStart:asc or timeStart:desc | matchOrder | :222 |
400 | invalid_request | Invalid role: must be one of: TOP, JUNGLE, MID, BOT, UTILITY | role | :168 |
400 | invalid_request | Invalid role: is not supported by the selected queue | validateRole | :177 |
404 | not_found | Profile not found | LolApiResponses NOT_FOUND | LolApiResponses.java:24 |
404 | not_found | Match not found | MatchService.getDetail | LolController.java:287 |
404 | not_found | Endpoint not found | NoHandlerFoundException | LolApiExceptionHandler.java:67 |
405 | method_not_allowed | HTTP method 'POST' is not supported for this endpoint | HttpRequestMethodNotSupportedException | :58 |
202 | *_pending (see below) | Profile initialization is pending | LolApiResponses PENDING | LolApiResponses.java:22 |
500 | internal_error | Unexpected server error | Exception | LolApiExceptionHandler.java:76 |
codeFor (LolApiExceptionHandler.java:89):
private String codeFor(HttpStatusCode status) {
return switch (status.value()) {
case 400 -> "invalid_request";
case 404 -> "not_found";
case 405 -> "method_not_allowed";
default -> "request_failed";
};
}
Validation — LolApiParameters.invalid
// LolApiParameters.java:235
public static ResponseStatusException invalid(String parameter, String reason) {
return new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid " + parameter + ": " + reason);
}
All validation methods (rank, queue, patch, page, limit, matchLimit, matchOffset, matchOrder, activityFilter, otpChampion, recordMetric, role) throw invalid → 400 with prefix Invalid {parameter}:.
202 pending — codes
LolApiResponses.from (src/main/java/com/safjnest/spring/controller/LolApiResponses.java:14):
case PENDING -> ResponseEntity.status(HttpStatus.ACCEPTED)
.body(new LolApiError(HttpStatus.ACCEPTED.value(), pendingCode, pendingMessage, result.metadata()));
Each controller passes its own pendingCode/pendingMessage:
code | message | Controller:line | Meaning | Retry |
|---|---|---|---|---|
profile_pending | Profile initialization is pending | LolController.java:292 | SummonerView not yet computed, job in ComputeScheduler/RiotScheduler | 3-5s |
profile_activity_pending | Profile activity is being prepared | :178 | ProfileActivity computing | 2-3s |
profile_matchups_pending | Profile matchups are being prepared | :202 | ProfileMatchups computing | 2-3s |
profile_records_pending | Profile records are being prepared | :214 | ProfileRecordPage for profile computing | 2-3s |
match_pending | Match analysis is pending | :285 | Match not in Mongo, queued on SyncScheduler | 3-5s |
leaderboard_pending | Leaderboard data is being prepared | LeaderboardController.java:54 | LeaderboardPage rebuilding ZSET | 2-3s |
champion_tier_list_pending | Champion tier list is being prepared | ChampionController.java:45 | ChampionTierList computing CHAMPION | 5-10s |
champion_data_pending | Champion data is being prepared | :73 | ChampionView/ChampionStatistics computing | 5-10s |
champion_pending | generic | — | alias for champion | — |
metadata for 202 contains refresh=true and often lastUpdate/filter for debugging. GET /profile/{puuid} also adds header X-Profile-Refresh: true (LolController.java:294).
HTTP/1.1 202 Accepted
Content-Type: application/json
X-Profile-Refresh: true
{
"status": 202,
"code": "profile_activity_pending",
"message": "Profile activity is being prepared",
"metadata": { "lastUpdate": 1715434800000, "refresh": true, "filter": { "champion": 157 } }
}
Mapping — LolApiExceptionHandler
LolApiExceptionHandler (src/main/java/com/safjnest/spring/controller/LolApiExceptionHandler.java:20) with @RestControllerAdvice:
| Handler | Exception | Return | File:line |
|---|---|---|---|
handleResponseStatus | ResponseStatusException | error(status, codeFor(status), reason) | :22 |
handleMissingParameter | MissingServletRequestParameterException | 400 invalid_request Missing required query parameter 'x' | :31 |
handleMissingPathVariable | MissingPathVariableException | 400 invalid_request Missing required path parameter 'x' | :40 |
handleTypeMismatch | MethodArgumentTypeMismatchException | 400 invalid_request Invalid value for parameter 'x' | :49 |
handleMethodNotSupported | HttpRequestMethodNotSupportedException | 405 method_not_allowed HTTP method 'X' is not supported | :58 |
handleNotFound | NoHandlerFoundException, NoResourceFoundException | 404 not_found Endpoint not found | :67 |
handleAsyncRequestNotUsable | AsyncRequestNotUsableException | void (client disconnected) | :72 |
handleUnexpected | Exception | 500 internal_error Unexpected server error + printStackTrace() | :76 |
error helper (:82):
private ResponseEntity<LolApiError> error(HttpStatusCode status, String code, String message) {
String errorMessage = message != null && !message.isBlank() ? message : HttpStatus.valueOf(status.value()).getReasonPhrase();
return ResponseEntity.status(status).body(new LolApiError(status.value(), code, errorMessage));
}
JSON examples
400 — validation
{
"status": 400,
"code": "invalid_request",
"message": "Invalid patch: must have the form major.minor",
"metadata": null
}
{
"status": 400,
"code": "invalid_request",
"message": "Invalid limit: must be between 1 and 50",
"metadata": null
}
404 — not found
{
"status": 404,
"code": "not_found",
"message": "Profile not found",
"metadata": null
}
{
"status": 404,
"code": "not_found",
"message": "Endpoint not found",
"metadata": null
}
405 — method not allowed
{
"status": 405,
"code": "method_not_allowed",
"message": "HTTP method 'POST' is not supported for this endpoint",
"metadata": null
}
202 — pending (with metadata)
{
"status": 202,
"code": "match_pending",
"message": "Match analysis is pending",
"metadata": { "lastUpdate": null, "refresh": true, "filter": null }
}
500 — internal
{
"status": 500,
"code": "internal_error",
"message": "Unexpected server error",
"metadata": null
}
Client handling
async function callApi(url) {
const res = await fetch(url);
if (res.status === 202) {
const err = await res.json(); // LolApiError with code *_pending
console.log("pending:", err.code, err.metadata);
return { pending: true, err };
}
if (res.status === 400) {
const err = await res.json();
throw new Error(`Bad request: ${err.message} (${err.code})`);
}
if (res.status === 404) throw new Error("Not found");
if (res.status === 405) throw new Error("Method not allowed");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return { pending: false, data: await res.json() };
}
Always handle
202as "data being prepared" — show skeleton/loading and retry with backoff. Do not treat202as an error.
