Skip to main content

3. Response Format and Error Handling

All calls to the V5 API return a Type DTO or boolean/long. In the event of a failure, the following safe default values are returned.

3.1 V5 Response Patterns​

Method PatternSuccess ReturnFailure Return
create(req) / update(id, req) / get(id)*ResponseV5 Instancenull
list() SeriesList<*ResponseV5>Empty List (Never null)
delete(id)truefalse
exists(id)true (Exists)false (Non-existent or Error)
count() Serieslong (≥0)0
Order Lifecycle (start/end/pause/resume/abort)true (Success)false (Transition Impossible, etc.)

Exceptions are not thrown. HTTP errors, server ERRORs, and JSON parsing failures are all absorbed into the safe default values shown in the table above. Error causes can be verified in the debug logs or the envelope's _code/_message.

3.2 Internal Wire Format (Reference)​

The V5 server responds in the following envelope format. Since the client library parses this automatically, general users do not need to concern themselves with it.

// 단건 성공
{ "_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 Mapping​

When the server sends an ERROR response, the following codes are populated in the _code field. The authoritative source is the product's plantpulse.core.exception.ErrorCode, and the HTTP status is also determined by that enumeration — since the code and status are paired, you may branch based on either one.

CodeHTTPNameCommon Cause
E1000400INVALID_REQUESTSyntax error, type mismatch
E1001400MISSING_PARAMETERRequired parameter missing
E1002422VALIDATION_FAILEDValue range, format, or domain rule violation (e.g., ID prefix)
E1100401UNAUTHORIZEDapi_key missing or invalid
E1101429AUTH_TOO_MANY_ATTEMPTSAccumulated authentication failures from the same IP — see Retry-After
E1102401TOKEN_EXPIREDToken expired — recoverable via re-issuance
E1103401TOKEN_REVOKEDToken revoked — the same token cannot be recovered
E1104401TOKEN_IP_MISMATCHIP bound to token differs from calling IP
E1200403FORBIDDENAuthenticated but lacking permissions
E1300404NOT_FOUNDCorresponding ID not found
E1400405METHOD_NOT_ALLOWEDMethod not allowed
E1500409CONFLICTResource conflict
E1501409DUPLICATE_ENTITYEntity already exists
E1600413PAYLOAD_TOO_LARGERequest body too large
E1700415UNSUPPORTED_MEDIA_TYPEUnsupported media type
E1800422IDEMPOTENCY_CONFLICTSame Idempotency-Key + different body
E1801409IDEMPOTENCY_IN_PROGRESSSame key is being processed — retry after a short delay
E1900429RATE_LIMIT_EXCEEDEDRate limit exceeded
E5000500INTERNAL_ERRORServer internal error
E5100500SERVICE_ERRORService processing error
E5200500DAO_ERRORData access error
E5300500ENGINE_ERROREngine processing error
E5400502EXTERNAL_SERVICE_ERRORExternal system call failure
E5500503SERVICE_UNAVAILABLEService unavailable — this is a "failure to execute," not a "determination." See Retry-After
E5503503SHUTTING_DOWNServer shutting down — hand over to another node or wait for Retry-After
E5600504TIMEOUTServer-side request timeout
There are two 429s — mixing them prevents recovery

E1900 (Rate Limit) is resolved by slowing down. E1101 (Too Many Auth Attempts) is not resolved by slowing down — the credentials themselves are incorrect, and that IP is locked until the window closes. If both are grouped under the same backoff, the latter will retry indefinitely.

Distinction between 400 and 422

400 means the request cannot be «interpreted», while 422 means it was interpreted but the «content» does not meet the rules (as of 2026-08-31). The status code distinguishes whether the format needs to be fixed or the value needs to be changed — humans read the message, but agents determine the next attempt based on the status code.

Common Rejections​

HTTPreasonMeaning
422asset_statement.eql.missing_columnsThe statement EQL does not provide a required field for this type — Equipment Statement
422asset_statement.eql.unknown_typeNon-existent statement type
422alarm.invalid.alarm_config_idA manually assigned ID was provided for alarm setting creation. Receive via GET /alarm/config/new-id
409—Attempted to create a second band alarm setting for the same tag. Only one per tag

3.4 Extracting Error Information​

V5 service methods do not expose the envelope directly, so error details are verified using the following two methods.

Method 1 — Debug Mode​

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("등록 실패 — 디버그 로그 확인");
}

Console output:

[V5] POST /api/v5/site body={...}
[V5] response: 400 {"_status":"ERROR","_code":"E1002","_message":"site_id must start with SITE_"}

Method 2 — Using BaseServiceV5 Static Methods (Custom Extension)​

The helper in BaseServiceV5 extracts error information from JSONObject responses. This is useful when creating a custom service that captures the envelope by extending the library.

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 Safe Call Patterns​

CRUD Calls​

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 Calls​

List<SiteResponseV5> sites = client.site().list();
// 실패해도 null 이 아님 — 그대로 순회 가능
for (SiteResponseV5 s : sites) {
System.out.println(s.getSite_id());
}
if (sites.isEmpty()) {
log.info("등록된 사이트 없음");
}

Existence Check → Retrieval​

if (client.site().exists("SITE_DJ")) {
SiteResponseV5 site = client.site().get("SITE_DJ");
// ...
}

Order Lifecycle (Boolean Return)​

String orderId = "ORDER_000001";

if (!client.order().start(orderId)) {
// 전이 불가 (예: 이미 START 상태)
log.warn("작업 시작 실패 — 현재 상태 확인");
return;
}

// 작업 진행 ...

if (!client.order().end(orderId)) {
log.warn("작업 종료 실패");
}

3.6 Retry Patterns​

V5 does not perform automatic retries. If retries are needed for transient network errors, you must implement them directly.

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 and Retry-After​

When the server cannot accept requests due to restarting or overload, Retry-After (integer in seconds) is provided. Java clients adhere to this header but will not wait longer than 30 seconds by default. When calling directly, follow these guidelines:

ScenarioResponse
Web layer is down (Restart · Redeployment) — Proxy responds503 E5500, Retry-After: 15
Server shutdown in progress503 E5503, Retry-After: 15
Backend storage temporarily unavailable (DB restart, etc.)503 E5500, Retry-After: 15
Per-token request limit exceeded429 E1900, Retry-After calculates the limit
Stream concurrency limit exceeded429 E1900 or 503 E5500, Retry-After
  • For Reads, wait for the specified duration and call again.
  • For Writes, call again with the same Idempotency-Key. A request interrupted by a restart may have already been applied.
  • 500 indicates a failure, and 502 E5400 indicates an external call failure. Neither provides Retry-After.

3.7 Distinguishing Empty Responses and Null Responses​

In V5, null or false can imply both of the following:

  1. No data on server (NOT_FOUND, E1300)
  2. Call failed (Network error, validation failure, etc.)

To distinguish between these, use exists().

if (!client.site().exists("SITE_DJ")) {
log.info("사이트가 등록되어 있지 않습니다");
// 신규 등록 로직
} else {
SiteResponseV5 site = client.site().get("SITE_DJ");
if (site == null) {
log.error("사이트는 존재하지만 조회 실패 — 일시적 오류 가능");
}
}

Next Steps​