mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Preserve route proxy rate-limit semantics
Narrow the Naver Maps proxy contract to JSON reverse geocode responses and preserve upstream quota signals so client fallback can make accurate decisions. Constraint: PR #282 review requested TDD fixes for XML contract mismatch, upstream 429 mapping, lint coverage, and route option documentation. Rejected: XML passthrough in this follow-up | It would require a separate response-shaping contract and tests beyond the JSON proxy boundary. Confidence: high Scope-risk: narrow Directive: Keep Naver Maps auth failures sanitized as 503 without upstream body snippets while preserving non-auth diagnostic snippets. Tested: node --test packages/k-skill-proxy/test/server.test.js; node --test scripts/skill-docs.test.js; bash scripts/validate-skills.sh; PYENV_VERSION=3.12.0 npm run ci; architect verification CLEAR Not-tested: Live NCP Maps calls with production credentials Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
parent
73c3611e8a
commit
68bd64ebd4
5 changed files with 51 additions and 6 deletions
|
|
@ -45,7 +45,7 @@
|
|||
4. live 모드:
|
||||
- 주소만 있으면 `/v1/naver-map/geocode` 로 좌표를 얻는다.
|
||||
- `/v1/naver-map/directions` 로 경로를 조회한다.
|
||||
- 응답의 `route.trafast[0].summary` 를 거리/시간/통행료/연료비로 매핑한다.
|
||||
- 기본 `option=trafast` 응답은 `route.trafast[0].summary` 를, 다른 option을 명시한 경우 `route[option][0].summary` 를 거리/시간/통행료/연료비로 매핑한다.
|
||||
5. live 실패(503/502/네트워크) 시 mock fallback 으로 떨어지고, 사용자에게 fallback 임을 명시한다.
|
||||
|
||||
## 예시
|
||||
|
|
@ -90,6 +90,7 @@ curl -fsS --get "${BASE}/v1/naver-map/directions" \
|
|||
|
||||
- 키 누락(`503 upstream_not_configured`) → mock fallback + 사용자에게 안내
|
||||
- 인증 실패(401/403) → proxy 가 `503` 으로 변환 → mock fallback
|
||||
- quota/rate-limit(429) → proxy 가 `429 upstream_error` 로 보존 → mock fallback + 재시도 간격 안내
|
||||
- 경로 미발견(`code != 0`) → `502 upstream_semantic_error` → 메시지와 함께 안내
|
||||
- 네트워크 실패 → `502 upstream_error` → mock fallback
|
||||
- 좌표 형식 오류 → `400 bad_request`
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ curl -fsS --get "${BASE}/v1/naver-map/directions" \
|
|||
--data-urlencode 'option=trafast'
|
||||
```
|
||||
|
||||
응답에서 `route.trafast[0].summary` 를 읽어 다음으로 매핑한다:
|
||||
응답에서 기본 `option=trafast` 기준 `route.trafast[0].summary` 를 읽고, 다른 option을 명시한 경우 `route[option][0].summary` 를 다음으로 매핑한다:
|
||||
|
||||
- `distance` (meter) → `distance_km = distance / 1000`
|
||||
- `duration` (millisecond) → `duration_minutes = duration / 60000`
|
||||
|
|
@ -159,6 +159,7 @@ curl -fsS --get "${BASE}/v1/naver-map/geocode" \
|
|||
|
||||
- proxy upstream key 미설정 (`NAVER_MAP_CLIENT_ID/SECRET` 없음) → `503 upstream_not_configured` → mock fallback
|
||||
- NCP Maps 인증 실패 (401/403) → proxy가 `503` 으로 변환 → mock fallback
|
||||
- NCP Maps quota/rate-limit (`429`) → proxy가 `429 upstream_error` 로 보존 → mock fallback + 재시도 간격 안내
|
||||
- 경로 미발견 (`code != 0`) → `502 upstream_semantic_error` → 메시지와 함께 안내
|
||||
- 좌표 형식 오류 → `400 bad_request`
|
||||
- 네트워크 실패 → `502 upstream_error` → mock fallback
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "node --check src/airkorea.js && node --check src/bluer.js && node --check src/hrfco.js && node --check src/krx-stock.js && node --check src/kstartup.js && node --check src/lh-notice.js && node --check src/mfds.js && node --check src/molit.js && node --check src/naver-news.js && node --check src/naver-shopping.js && node --check src/nts-business.js && node --check src/parking-lots.js && node --check src/region-lookup.js && node --check src/server.js && node --check test/airkorea.test.js && node --check test/hrfco.test.js && node --check test/lh-notice.test.js && node --check test/molit.test.js && node --check test/naver-news.test.js && node --check test/naver-shopping.test.js && node --check test/region-lookup.test.js && node --check test/server.test.js",
|
||||
"lint": "node --check src/airkorea.js && node --check src/bluer.js && node --check src/hrfco.js && node --check src/krx-stock.js && node --check src/kstartup.js && node --check src/lh-notice.js && node --check src/mfds.js && node --check src/molit.js && node --check src/naver-map.js && node --check src/naver-news.js && node --check src/naver-shopping.js && node --check src/nts-business.js && node --check src/parking-lots.js && node --check src/region-lookup.js && node --check src/server.js && node --check test/airkorea.test.js && node --check test/hrfco.test.js && node --check test/lh-notice.test.js && node --check test/molit.test.js && node --check test/naver-news.test.js && node --check test/naver-shopping.test.js && node --check test/region-lookup.test.js && node --check test/server.test.js",
|
||||
"test": "node --test"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ function createNaverMapHttpError(serviceName, responseStatus, bodyText) {
|
|||
const error = new Error(`Naver Maps ${serviceName} upstream returned an error.`);
|
||||
error.code = "upstream_error";
|
||||
const isAuthError = responseStatus === 401 || responseStatus === 403;
|
||||
error.statusCode = isAuthError ? 503 : 502;
|
||||
error.statusCode = isAuthError ? 503 : responseStatus === 429 ? 429 : 502;
|
||||
error.upstreamStatusCode = responseStatus;
|
||||
if (!isAuthError) {
|
||||
error.upstreamBodySnippet = bodyText.slice(0, 200);
|
||||
|
|
@ -136,8 +136,8 @@ function normalizeNaverMapReverseGeocodeQuery(query) {
|
|||
}
|
||||
|
||||
const output = trimOrNull(query.output) || "json";
|
||||
if (output !== "json" && output !== "xml") {
|
||||
throw new Error("Provide output as json or xml.");
|
||||
if (output !== "json") {
|
||||
throw new Error("Provide output as json. XML passthrough is not supported by this proxy.");
|
||||
}
|
||||
|
||||
return { coords, orders, output };
|
||||
|
|
|
|||
|
|
@ -909,6 +909,41 @@ test("Naver Map endpoints sanitize upstream auth errors as 503 without leaking t
|
|||
}
|
||||
});
|
||||
|
||||
test("Naver Map endpoints preserve upstream 429 for caller backoff", async (t) => {
|
||||
const originalFetch = global.fetch;
|
||||
global.fetch = async () => new Response("quota exceeded diagnostic", {
|
||||
status: 429,
|
||||
headers: { "content-type": "text/plain" }
|
||||
});
|
||||
|
||||
const app = buildServer({
|
||||
env: {
|
||||
NAVER_MAP_CLIENT_ID: "id",
|
||||
NAVER_MAP_CLIENT_SECRET: "secret"
|
||||
}
|
||||
});
|
||||
|
||||
t.after(async () => {
|
||||
global.fetch = originalFetch;
|
||||
await app.close();
|
||||
});
|
||||
|
||||
const cases = [
|
||||
"/v1/naver-map/directions?start=126.9,37.5&goal=127.0,37.5",
|
||||
"/v1/naver-map/geocode?q=%EC%84%9C%EC%9A%B8%EC%97%AD",
|
||||
"/v1/naver-map/reverse-geocode?coords=126.9,37.5"
|
||||
];
|
||||
|
||||
for (const url of cases) {
|
||||
const response = await app.inject({ method: "GET", url });
|
||||
assert.equal(response.statusCode, 429);
|
||||
const body = response.json();
|
||||
assert.equal(body.error, "upstream_error");
|
||||
assert.equal(body.upstream.status_code, 429);
|
||||
assert.equal(body.upstream.body_snippet, "quota exceeded diagnostic");
|
||||
}
|
||||
});
|
||||
|
||||
test("Naver Map directions endpoint keeps non-auth upstream snippets for diagnostics", async (t) => {
|
||||
const originalFetch = global.fetch;
|
||||
global.fetch = async () => new Response("Transient upstream diagnostic", {
|
||||
|
|
@ -1019,6 +1054,14 @@ test("Naver Map reverse-geocode endpoint validates coords and orders", async (t)
|
|||
url: "/v1/naver-map/reverse-geocode?coords=127.0,37.5&orders=banana"
|
||||
});
|
||||
assert.equal(badOrder.statusCode, 400);
|
||||
|
||||
const xmlOutput = await app.inject({
|
||||
method: "GET",
|
||||
url: "/v1/naver-map/reverse-geocode?coords=127.0,37.5&output=xml"
|
||||
});
|
||||
assert.equal(xmlOutput.statusCode, 400);
|
||||
assert.equal(xmlOutput.json().error, "bad_request");
|
||||
assert.match(xmlOutput.json().message, /output as json/);
|
||||
});
|
||||
|
||||
test("Naver Map health endpoint reflects naverMapConfigured flag", async (t) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue