mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: add CLI-server protocol version mismatch detection
- Define PROTOCOL_VERSION constant in shared/src/version.ts - Server sets X-Hapi-Protocol-Version header on /cli/* responses - Server includes protocolVersion in /health endpoint - CLI extracts serverProtocolVersion from API responses - CLI shows version mismatch hints in both directions - Add tests for error utilities and version extraction close #104
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { extractErrorInfo, apiValidationError } from './errorUtils'
|
||||
|
||||
describe('extractErrorInfo', () => {
|
||||
it('extracts serverProtocolVersion from axios-style response header', () => {
|
||||
const error = {
|
||||
message: 'Request failed with status code 400',
|
||||
response: {
|
||||
status: 400,
|
||||
headers: { 'x-hapi-protocol-version': '2' },
|
||||
data: { error: 'Invalid body' }
|
||||
}
|
||||
}
|
||||
const info = extractErrorInfo(error)
|
||||
expect(info.serverProtocolVersion).toBe(2)
|
||||
expect(info.httpStatus).toBe(400)
|
||||
expect(info.responseErrorText).toBe('Invalid body')
|
||||
})
|
||||
|
||||
it('extracts serverProtocolVersion from direct property (apiValidationError)', () => {
|
||||
const error = new Error('Invalid /cli/machines response')
|
||||
;(error as unknown as Record<string, unknown>).serverProtocolVersion = 1
|
||||
const info = extractErrorInfo(error)
|
||||
expect(info.serverProtocolVersion).toBe(1)
|
||||
expect(info.message).toBe('Invalid /cli/machines response')
|
||||
})
|
||||
|
||||
it('prefers direct property over header', () => {
|
||||
const error = Object.assign(new Error('test'), {
|
||||
serverProtocolVersion: 3,
|
||||
response: {
|
||||
status: 200,
|
||||
headers: { 'x-hapi-protocol-version': '5' },
|
||||
data: {}
|
||||
}
|
||||
})
|
||||
const info = extractErrorInfo(error)
|
||||
expect(info.serverProtocolVersion).toBe(3)
|
||||
})
|
||||
|
||||
it('returns undefined serverProtocolVersion when neither source present', () => {
|
||||
const error = new Error('some error')
|
||||
const info = extractErrorInfo(error)
|
||||
expect(info.serverProtocolVersion).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles non-numeric protocol header gracefully', () => {
|
||||
const error = {
|
||||
message: 'fail',
|
||||
response: {
|
||||
status: 200,
|
||||
headers: { 'x-hapi-protocol-version': 'abc' },
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
const info = extractErrorInfo(error)
|
||||
expect(info.serverProtocolVersion).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('apiValidationError', () => {
|
||||
it('creates error with serverProtocolVersion from response header', () => {
|
||||
const fakeResponse = {
|
||||
headers: { 'x-hapi-protocol-version': '1' }
|
||||
}
|
||||
const err = apiValidationError('Invalid /cli/machines response', fakeResponse as any)
|
||||
expect(err.message).toBe('Invalid /cli/machines response')
|
||||
expect((err as any).serverProtocolVersion).toBe(1)
|
||||
})
|
||||
|
||||
it('creates error without serverProtocolVersion when header missing', () => {
|
||||
const fakeResponse = { headers: {} }
|
||||
const err = apiValidationError('Invalid /cli/sessions response', fakeResponse as any)
|
||||
expect(err.message).toBe('Invalid /cli/sessions response')
|
||||
expect((err as any).serverProtocolVersion).toBeUndefined()
|
||||
})
|
||||
|
||||
it('round-trips through extractErrorInfo', () => {
|
||||
const fakeResponse = {
|
||||
headers: { 'x-hapi-protocol-version': '2' }
|
||||
}
|
||||
const err = apiValidationError('Invalid /cli/machines response', fakeResponse as any)
|
||||
const info = extractErrorInfo(err)
|
||||
expect(info.serverProtocolVersion).toBe(2)
|
||||
expect(info.messageLower).toContain('invalid /cli/')
|
||||
})
|
||||
})
|
||||
@@ -2,12 +2,31 @@
|
||||
* Error handling utilities for API requests
|
||||
*/
|
||||
|
||||
import type { AxiosResponse } from 'axios'
|
||||
|
||||
export type ErrorInfo = {
|
||||
message: string
|
||||
messageLower: string
|
||||
axiosCode?: string
|
||||
httpStatus?: number
|
||||
responseErrorText: string
|
||||
serverProtocolVersion?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Error for a successful HTTP response whose body fails schema validation.
|
||||
* Attaches the protocol version header so callers can detect version mismatch.
|
||||
*/
|
||||
export function apiValidationError(message: string, response: AxiosResponse): Error {
|
||||
const err = new Error(message)
|
||||
const raw = response.headers?.['x-hapi-protocol-version']
|
||||
if (raw != null) {
|
||||
const pv = Number(raw)
|
||||
if (Number.isFinite(pv)) {
|
||||
;(err as unknown as Record<string, unknown>).serverProtocolVersion = pv
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,12 +52,29 @@ export function extractErrorInfo(error: unknown): ErrorInfo {
|
||||
: undefined
|
||||
const responseErrorText = typeof responseError === 'string' ? responseError : ''
|
||||
|
||||
// Protocol version: prefer direct property (set by apiValidationError),
|
||||
// fall back to axios response header (set by server on error responses)
|
||||
let serverProtocolVersion: number | undefined
|
||||
if (typeof record.serverProtocolVersion === 'number' && Number.isFinite(record.serverProtocolVersion)) {
|
||||
serverProtocolVersion = record.serverProtocolVersion
|
||||
} else {
|
||||
const headers = typeof response?.headers === 'object' && response.headers !== null
|
||||
? (response.headers as Record<string, unknown>)
|
||||
: undefined
|
||||
const protocolHeader = headers?.['x-hapi-protocol-version']
|
||||
if (typeof protocolHeader === 'string' && protocolHeader !== '') {
|
||||
const pv = Number(protocolHeader)
|
||||
if (Number.isFinite(pv)) serverProtocolVersion = pv
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
messageLower,
|
||||
axiosCode,
|
||||
httpStatus,
|
||||
responseErrorText
|
||||
responseErrorText,
|
||||
serverProtocolVersion
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user