3. 応答形式およびエラー処理
V5 APIのすべての呼び出しはタイプ DTOまたはboolean/longを返します。失敗時には次のような安全なデフォルト値を返します。
3.1 V5 応答パターン
| メソッドパターン | 成功時の返り値 | 失敗時の返り値 |
|---|---|---|
create(req) / update(id, req) / get(id) | *ResponseV5 インスタンス | null |
list() 形式 | List<*ResponseV5> | 空リスト (絶対に null ではない) |
delete(id) | true | false |
exists(id) | true (あり) | false (なしまたはエラー) |
count() 形式 | long (≥0) | 0 |
Order ライフサイクル (start/end/pause/resume/abort) | true (成功) | false (遷移不可など) |
例外はスローされません。 HTTPエラー、サーバーERROR、JSONパース失敗はすべて上記の表にある安全なデフォルト値に吸収されます。エラーの原因はデバッグログまたはenvelopeの
_code/_messageで確認します。
3.2 内部 wire format (参考)
V5 サーバーは次のような envelope 形式で応答します。クライアントライブラリが自動的にパースするため、通常の使用時に意識する必要はありません。
// 단건 성공
{ "_status": "OK", "data": { ... } }
// 목록 성공
{ "_status": "OK", "data": [ { ... }, { ... } ] }
// 목록 — items 키 안에 배열
{ "_status": "OK", "data": { "items": [ ... ] } }
// 에러
{
"_status": "ERROR",
"_code": "E1002",
"_message": "validation failed: ...",
"_http_status": 400
}
3.3 V5 ErrorCode マッピング
サーバーが ERROR 応答を送信するとき、_code フィールドに次のコードが含まれます。正本は製品の plantpulse.core.exception.ErrorCode であり、HTTP ステータスもその列挙型と共に決定されます。コードとステータスは対であるため、どちらか一方のみを見て分岐させても問題ありません。
| コード | HTTP | 名前 | 一般的な原因 |
|---|---|---|---|
E1000 | 400 | INVALID_REQUEST | 文法エラー、タイプ不一致 |
E1001 | 400 | MISSING_PARAMETER | 必須パラメータの不足 |
E1002 | 422 | VALIDATION_FAILED | 値の範囲・形式・ドメイン規則違反 (ID接頭辞など) |
E1100 | 401 | UNAUTHORIZED | api_key の不足または無効 |
E1101 | 429 | AUTH_TOO_MANY_ATTEMPTS | 同一IPの認証失敗蓄積 — Retry-After を参照 |
E1102 | 401 | TOKEN_EXPIRED | トークン期限切れ — 再発行で復旧します |
E1103 | 401 | TOKEN_REVOKED | トークン回収 — 同一トークンでは復旧しません |
E1104 | 401 | TOKEN_IP_MISMATCH | トークンに紐付けられたIPと呼び出しIPが不一致 |
E1200 | 403 | FORBIDDEN | 認証済みだが権限なし |
E1300 | 404 | NOT_FOUND | 該当IDなし |
E1400 | 405 | METHOD_NOT_ALLOWED | 許可されていないメソッド |
E1500 | 409 | CONFLICT | リソース競合 |
E1501 | 409 | DUPLICATE_ENTITY | 既に存在するエンティティ |
E1600 | 413 | PAYLOAD_TOO_LARGE | リクエストボディが過大 |
E1700 | 415 | UNSUPPORTED_MEDIA_TYPE | サポートされていないメディアタイプ |
E1800 | 422 | IDEMPOTENCY_CONFLICT | 同一 Idempotency-Key + 異なるbody |
E1801 | 409 | IDEMPOTENCY_IN_PROGRESS | 同一キーを処理中 — しばらくして再試行 |
E1900 | 429 | RATE_LIMIT_EXCEEDED | レートリミット超過 |
E5000 | 500 | INTERNAL_ERROR | サーバー内部エラー |
E5100 | 500 | SERVICE_ERROR | サービス処理エラー |
E5200 | 500 | DAO_ERROR | データアクセスエラー |
E5300 | 500 | ENGINE_ERROR | エンジン処理エラー |
E5400 | 502 | EXTERNAL_SERVICE_ERROR | 外部システム呼び出し失敗 |
E5500 | 503 | SERVICE_UNAVAILABLE | サービス利用不可 — 判定ではなく「できなかった」ことを意味します。 Retry-After を参照 |
E5503 | 503 | SHUTTING_DOWN | サーバー停止中 — 他のノードに転送するか Retry-After 分待機します |
E5600 | 504 | TIMEOUT | サーバー側リクエストタイムアウト |
E1900 (レートリミット) は速度を落とせば解除されます。E1101 (認証試行過多) は速度を落としても解除されません。資格情報自体が誤っており、そのIPがウィンドウが閉じるまでロックされている状態です。これらを同じバックオフでまとめると、後者は永遠に再試行を繰り返すことになります。
400 はリクエストを「解釈」できないという意味であり、422 は解釈はできたが「内容」が規則に沿っていないという意味です(2026-08-31より)。形式を修正すべきか、値を修正すべきかがステータスコードで分かれます。人間はメッセージを読みますが、エージェントはステータスコードで次の試行を決定するためです。
よくある拒否事例
| HTTP | reason | 意味 |
|---|---|---|
| 422 | asset_statement.eql.missing_columns | ステートメント EQL がこの種類の必須項目を出力していません — 設備ステートメント |
| 422 | asset_statement.eql.unknown_type | 存在しないステートメント種類です |
| 422 | alarm.invalid.alarm_config_id | アラーム設定作成時に直接指定したIDが入力されました。GET /alarm/config/new-id で受け取ってください |
| 409 | — | 同一タグに2番目の帯域(バンド)アラーム設定を作成しようとしました。タグあたり1つのみです |
3.4 エラー情報の抽出
V5 サービスメソッドは envelope を直接公開しないため、エラーの詳細は次の2つの方法で確認します。
方法 1 — デバッグモード
APIClient_V5 client = new APIClient_V5(proto, host, port, user, token, true);
client.connect();
SiteResponseV5 result = client.site().create(req);
if (result == null) {
// 콘솔 로그에서 envelope 확인 가능
System.err.println("등록 실패 — 디버그 로그 확인");
}
コンソール出力:
[V5] POST /api/v5/site body={...}
[V5] response: 400 {"_status":"ERROR","_code":"E1002","_message":"site_id must start with SITE_"}
方法 2 — BaseServiceV5 static メソッドの使用 (カスタム拡張)
BaseServiceV5 のヘルパーは JSONObject 応答からエラー情報を抽出します。ライブラリを拡張して envelope をキャプチャするカスタムサービスを作成する場合に有用です。
import plantpulse.api.v5.service.BaseServiceV5;
import plantpulse.json.JSONObject;
// 환경 (envelope) JSONObject 가 있다면
String code = BaseServiceV5.getErrorCode(env); // 예: "E1002"
String message = BaseServiceV5.getErrorMessage(env); // 예: "validation failed: ..."
int httpStatus = BaseServiceV5.getHttpStatus(env); // 예: 400
3.5 安全な呼び出しパターン
CRUD 呼び出し
import plantpulse.api.v5.dto.request.SiteRequestV5;
import plantpulse.api.v5.dto.response.SiteResponseV5;
SiteRequestV5 req = new SiteRequestV5();
req.setSite_id("SITE_DJ");
req.setSite_name("대전공장");
SiteResponseV5 created = client.site().create(req);
if (created == null) {
// 실패 처리 — 예: 알람, 재시도, 다른 경로 시도
log.warn("사이트 생성 실패");
return;
}
System.out.println("등록 완료: " + created.getSite_id());
リスト呼び出し
List<SiteResponseV5> sites = client.site().list();
// 실패해도 null 이 아님 — 그대로 순회 가능
for (SiteResponseV5 s : sites) {
System.out.println(s.getSite_id());
}
if (sites.isEmpty()) {
log.info("등록된 사이트 없음");
}
存在確認 → 照会
if (client.site().exists("SITE_DJ")) {
SiteResponseV5 site = client.site().get("SITE_DJ");
// ...
}
Order ライフサイクル (boolean 返却)
String orderId = "ORDER_000001";
if (!client.order().start(orderId)) {
// 전이 불가 (예: 이미 START 상태)
log.warn("작업 시작 실패 — 현재 상태 확인");
return;
}
// 작업 진행 ...
if (!client.order().end(orderId)) {
log.warn("작업 종료 실패");
}
3.6 再試行パターン
V5 は自動再試行を行いません。一時的なネットワークエラーに備えた再試行が必要な場合は、直接実装してください。
import java.util.function.Supplier;
public static <T> T callWithRetry(Supplier<T> apiCall, int maxRetries, long baseDelayMs) {
int attempt = 0;
while (true) {
T result = apiCall.get();
if (result != null) return result; // 성공
if (++attempt >= maxRetries) return null; // 포기
try {
Thread.sleep(baseDelayMs * attempt); // 지수 백오프
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return null;
}
}
}
// 사용
SiteResponseV5 site = callWithRetry(
() -> client.site().get("SITE_DJ"),
3,
1_000L);
503 · 429 と Retry-After
再起動や過負荷で現在受け付けられない場合は、Retry-After (秒単位の整数) が共に送られてきます。Java クライアントはこのヘッダーを遵守しますが、基本として30秒を超えて待機することはありません。直接呼び出す場合は以下に従ってください。
| 状況 | 応答 |
|---|---|
| ウェブ層がダウンしている (再起動・再デプロイ) — プロキシが応答 | 503 E5500, Retry-After: 15 |
| サーバー停止進行中 | 503 E5503, Retry-After: 15 |
| 後方ストレージ一時不可 (DB再起動など) | 503 E5500, Retry-After: 15 |
| トークン別リクエスト上限超過 | 429 E1900, Retry-After で上限を計算 |
| ストリーム同時数超過 | 429 E1900 または 503 E5500, Retry-After |
- 読み取りは、指定時間待機してから再度呼び出します。
- 書き込みは、同一の
Idempotency-Keyで再度呼び出します。再起動で遮断されたリクエストは、既に反映されている可能性があります。 - 500 は故障であり、502
E5400は外部呼び出し失敗です。どちらもRetry-Afterは存在しません。
3.7 空応答と null 応答の区別
V5 において null または false は次の2つの意味を共に含みます:
- サーバーにデータがない (
NOT_FOUND,E1300) - 呼び出しに失敗した (ネットワークエラー、検証失敗など)
これらを区別するには、exists() を併用してください。
if (!client.site().exists("SITE_DJ")) {
log.info("사이트가 등록되어 있지 않습니다");
// 신규 등록 로직
} else {
SiteResponseV5 site = client.site().get("SITE_DJ");
if (site == null) {
log.error("사이트는 존재하지만 조회 실패 — 일시적 오류 가능");
}
}
次のステップ
- System サービス — ヘルスチェック
- Site サービス — マスタデータの起点