mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2025-02-23 07:09:35 +02:00
rework error handling 💥✅📝
This commit is contained in:
parent
0275b65c7a
commit
9b263bb379
9 changed files with 435 additions and 149 deletions
|
@ -135,6 +135,44 @@ const client = createClient({
|
||||||
|
|
||||||
The default `profile.logRequest` [`console.error`](https://nodejs.org/docs/latest-v10.x/api/console.html#console_console_error_data_args)s the request body, if you have set `$DEBUG` to `hafas-client`. Likewise, `profile.logResponse` `console.error`s the response body.
|
The default `profile.logRequest` [`console.error`](https://nodejs.org/docs/latest-v10.x/api/console.html#console_console_error_data_args)s the request body, if you have set `$DEBUG` to `hafas-client`. Likewise, `profile.logResponse` `console.error`s the response body.
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
Unexpected errors – e.g. due to bugs in `hafas-client` itself – aside, its methods may reject with the following errors:
|
||||||
|
|
||||||
|
- `HafasError` – A generic error to signal that something HAFAS-related went wrong, either in the client, or in the HAFAS endpoint.
|
||||||
|
- `HafasAccessDeniedError` – The HAFAS endpoint has rejected your request because you're not allowed to access it (or the specific API call). Subclass of `HafasError`.
|
||||||
|
- `HafasInvalidRequestError` – The HAFAS endpoint reports that an invalid request has been sent. Subclass of `HafasError`.
|
||||||
|
- `HafasNotFoundError` – The HAFAS endpoint does not know about such stop/trip/etc. Subclass of `HafasError`.
|
||||||
|
- `HafasServerError` – An error occured within the HAFAS endpoint, so that it can't fulfill the request; For example, this happens when HAFAS' internal backend is unavailable. Subclass of `HafasError`.
|
||||||
|
|
||||||
|
Each error has the following properties:
|
||||||
|
|
||||||
|
- `isHafasError` – Always `true`. Allows you to differente HAFAS-related errors from e.g. network errors.
|
||||||
|
- `code` – A string representing the error type for all other error classes, e.g. `INVALID_REQUEST` for `HafasInvalidRequestError`. `null` for plain `HafasError`s.
|
||||||
|
- `isCausedByServer` – Boolean, telling you if the HAFAS endpoint says that it couldn't process your request because *it* is unavailable/broken.
|
||||||
|
- `hafasCode` – A HAFAS-specific error code, if the HAFAS endpoint returned one; e.g. `H890` when no journeys could be found. `null` otherwise.
|
||||||
|
- `request` – The [Fetch API `Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) of the request.
|
||||||
|
- `url` – The URL of the request.
|
||||||
|
- `response` – The [Fetch API `Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
|
||||||
|
|
||||||
|
To check **if an error from `hafas-client` is HAFAS-specific, use `error instanceof HafasError`**:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const {HafasError} = require('hafas-client/lib/errors')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.journeys(/* … */)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof HafasError) {
|
||||||
|
// HAFAS-specific error
|
||||||
|
} else {
|
||||||
|
// different error, e.g. network (ETIMEDOUT, ENETDOWN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To determine **if you should automatically retry an error, use `!error.causedByServer`**.
|
||||||
|
|
||||||
## Using `hafas-client` from another language
|
## Using `hafas-client` from another language
|
||||||
|
|
||||||
If you want to use `hafas-client` to access HAFAS APIs but work with non-Node.js environments, you can use [`hafas-client-rpc`](https://github.com/derhuerst/hafas-client-rpc) to create a [JSON-RPC](https://www.jsonrpc.org) interface that you can send commands to.
|
If you want to use `hafas-client` to access HAFAS APIs but work with non-Node.js environments, you can use [`hafas-client-rpc`](https://github.com/derhuerst/hafas-client-rpc) to create a [JSON-RPC](https://www.jsonrpc.org) interface that you can send commands to.
|
||||||
|
|
25
index.js
25
index.js
|
@ -8,6 +8,7 @@ const defaultProfile = require('./lib/default-profile')
|
||||||
const validateProfile = require('./lib/validate-profile')
|
const validateProfile = require('./lib/validate-profile')
|
||||||
const {INVALID_REQUEST} = require('./lib/errors')
|
const {INVALID_REQUEST} = require('./lib/errors')
|
||||||
const sliceLeg = require('./lib/slice-leg')
|
const sliceLeg = require('./lib/slice-leg')
|
||||||
|
const {HafasError} = require('./lib/errors')
|
||||||
|
|
||||||
const isNonEmptyString = str => 'string' === typeof str && str.length > 0
|
const isNonEmptyString = str => 'string' === typeof str && str.length > 0
|
||||||
|
|
||||||
|
@ -248,11 +249,7 @@ const createClient = (profile, userAgent, opt = {}) => {
|
||||||
|
|
||||||
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
||||||
if (!Array.isArray(res.outConL) || !res.outConL[0]) {
|
if (!Array.isArray(res.outConL) || !res.outConL[0]) {
|
||||||
const err = new Error('invalid response')
|
throw new HafasError('invalid response, expected outConL[0]', null, {})
|
||||||
// technically this is not a HAFAS error
|
|
||||||
// todo: find a different flag with decent DX
|
|
||||||
err.isHafasError = true
|
|
||||||
throw err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = {profile, opt, common, res}
|
const ctx = {profile, opt, common, res}
|
||||||
|
@ -427,14 +424,10 @@ const createClient = (profile, userAgent, opt = {}) => {
|
||||||
|
|
||||||
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
||||||
if (!res || !Array.isArray(res.locL) || !res.locL[0]) {
|
if (!res || !Array.isArray(res.locL) || !res.locL[0]) {
|
||||||
// todo: proper stack trace?
|
throw new HafasError('invalid response, expected locL[0]', null, {
|
||||||
// todo: DRY with lib/request.js
|
// This problem occurs on invalid input. 🙄
|
||||||
const err = new Error('response has no stop')
|
code: INVALID_REQUEST,
|
||||||
// technically this is not a HAFAS error
|
})
|
||||||
// todo: find a different flag with decent DX
|
|
||||||
err.isHafasError = true
|
|
||||||
err.code = INVALID_REQUEST
|
|
||||||
throw err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = {profile, opt, res, common}
|
const ctx = {profile, opt, res, common}
|
||||||
|
@ -622,9 +615,9 @@ const createClient = (profile, userAgent, opt = {}) => {
|
||||||
|
|
||||||
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
const {res, common} = await profile.request({profile, opt}, userAgent, req)
|
||||||
if (!Array.isArray(res.posL)) {
|
if (!Array.isArray(res.posL)) {
|
||||||
const err = new Error('invalid response')
|
throw new HafasError('invalid response, expected posL[0]', null, {
|
||||||
err.shouldRetry = true
|
shouldRetry: true,
|
||||||
throw err
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const byDuration = []
|
const byDuration = []
|
||||||
|
|
44
lib/check-if-res-is-ok.js
Normal file
44
lib/check-if-res-is-ok.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const {HafasError, byErrorCode} = require('./errors')
|
||||||
|
|
||||||
|
const checkIfResponseIsOk = (_) => {
|
||||||
|
const {
|
||||||
|
body,
|
||||||
|
errProps: baseErrProps,
|
||||||
|
} = _
|
||||||
|
|
||||||
|
const errProps = {
|
||||||
|
...baseErrProps,
|
||||||
|
}
|
||||||
|
if (body.id) errProps.hafasResponseId = body.id
|
||||||
|
|
||||||
|
// Because we want more accurate stack traces, we don't construct the error here,
|
||||||
|
// but only return the constructor & error message.
|
||||||
|
const getError = (_) => {
|
||||||
|
// mutating here is ugly but pragmatic
|
||||||
|
if (_.errTxt) errProps.hafasMessage = _.errTxt
|
||||||
|
if (_.errTxtOut) errProps.hafasDescription = _.errTxtOut
|
||||||
|
|
||||||
|
if (_.err in byErrorCode) return byErrorCode[_.err]
|
||||||
|
return {
|
||||||
|
Error: HafasError,
|
||||||
|
message: body.errTxt || 'unknown error',
|
||||||
|
props: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.err && body.err !== 'OK') {
|
||||||
|
const {Error: HafasError, message, props} = getError(body)
|
||||||
|
throw new HafasError(message, body.err, {...errProps, ...props})
|
||||||
|
}
|
||||||
|
if (!body.svcResL || !body.svcResL[0]) {
|
||||||
|
throw new HafasError('invalid/unsupported response structure', null, errProps)
|
||||||
|
}
|
||||||
|
if (body.svcResL[0].err !== 'OK') {
|
||||||
|
const {Error: HafasError, message, props} = getError(body.svcResL[0])
|
||||||
|
throw new HafasError(message, body.svcResL[0].err, {...errProps, ...props})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = checkIfResponseIsOk
|
266
lib/errors.js
266
lib/errors.js
|
@ -5,205 +5,268 @@ const INVALID_REQUEST = 'INVALID_REQUEST'
|
||||||
const NOT_FOUND = 'NOT_FOUND'
|
const NOT_FOUND = 'NOT_FOUND'
|
||||||
const SERVER_ERROR = 'SERVER_ERROR'
|
const SERVER_ERROR = 'SERVER_ERROR'
|
||||||
|
|
||||||
|
class HafasError extends Error {
|
||||||
|
constructor (cleanMessage, hafasCode, props) {
|
||||||
|
const msg = hafasCode
|
||||||
|
? hafasCode + ': ' + cleanMessage
|
||||||
|
: cleanMessage
|
||||||
|
super(msg)
|
||||||
|
|
||||||
|
// generic props
|
||||||
|
this.isHafasError = true
|
||||||
|
|
||||||
|
// error-specific props
|
||||||
|
this.code = null
|
||||||
|
// By default, we take the blame, unless we know for sure.
|
||||||
|
this.isCausedByServer = false
|
||||||
|
this.hafasCode = hafasCode
|
||||||
|
Object.assign(this, props)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HafasAccessDeniedError extends HafasError {
|
||||||
|
constructor (cleanMessage, hafasCode, props) {
|
||||||
|
super(cleanMessage, hafasCode, props)
|
||||||
|
this.code = ACCESS_DENIED
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HafasInvalidRequestError extends HafasError {
|
||||||
|
constructor (cleanMessage, hafasCode, props) {
|
||||||
|
super(cleanMessage, hafasCode, props)
|
||||||
|
this.code = INVALID_REQUEST
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HafasNotFoundError extends HafasError {
|
||||||
|
constructor (cleanMessage, hafasCode, props) {
|
||||||
|
super(cleanMessage, hafasCode, props)
|
||||||
|
this.code = NOT_FOUND
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HafasServerError extends HafasError {
|
||||||
|
constructor (cleanMessage, hafasCode, props) {
|
||||||
|
super(cleanMessage, hafasCode, props)
|
||||||
|
this.code = SERVER_ERROR
|
||||||
|
this.isCausedByServer = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// https://gist.github.com/derhuerst/79d49c0f04c1c192a5d15756e5af575f/edit
|
// https://gist.github.com/derhuerst/79d49c0f04c1c192a5d15756e5af575f/edit
|
||||||
// todo:
|
// todo:
|
||||||
// `code: 'METHOD_NA', message: 'HCI Service: service method disabled'`
|
// `code: 'METHOD_NA', message: 'HCI Service: service method disabled'`
|
||||||
// "err": "PARAMETER", "errTxt": "HCI Service: parameter invalid"
|
// "err": "PARAMETER", "errTxt": "HCI Service: parameter invalid"
|
||||||
|
// "err": "PARAMETER", "errTxt": "HCI Service: parameter invalid","errTxtOut":"Während der Suche ist ein interner Fehler aufgetreten"
|
||||||
const byErrorCode = Object.assign(Object.create(null), {
|
const byErrorCode = Object.assign(Object.create(null), {
|
||||||
H_UNKNOWN: {
|
H_UNKNOWN: {
|
||||||
isServer: false,
|
Error: HafasError,
|
||||||
code: SERVER_ERROR,
|
|
||||||
message: 'unknown internal error',
|
message: 'unknown internal error',
|
||||||
statusCode: 500,
|
props: {
|
||||||
|
shouldRetry: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
AUTH: {
|
AUTH: {
|
||||||
isClient: true,
|
Error: HafasAccessDeniedError,
|
||||||
code: ACCESS_DENIED,
|
|
||||||
message: 'invalid or missing authentication data',
|
message: 'invalid or missing authentication data',
|
||||||
statusCode: 401
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
R0001: {
|
R0001: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'unknown method',
|
message: 'unknown method',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
R0002: {
|
R0002: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'invalid or missing request parameters',
|
message: 'invalid or missing request parameters',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
R0007: {
|
R0007: {
|
||||||
isServer: true,
|
Error: HafasServerError,
|
||||||
code: SERVER_ERROR,
|
|
||||||
message: 'internal communication error',
|
message: 'internal communication error',
|
||||||
statusCode: 500
|
props: {
|
||||||
|
shouldRetry: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
R5000: {
|
R5000: {
|
||||||
isClient: true,
|
Error: HafasAccessDeniedError,
|
||||||
code: ACCESS_DENIED,
|
|
||||||
message: 'access denied',
|
message: 'access denied',
|
||||||
statusCode: 401
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
S1: {
|
S1: {
|
||||||
isServer: true,
|
Error: HafasServerError,
|
||||||
code: SERVER_ERROR,
|
|
||||||
message: 'journeys search: a connection to the backend server couldn\'t be established',
|
message: 'journeys search: a connection to the backend server couldn\'t be established',
|
||||||
statusCode: 503
|
props: {
|
||||||
|
shouldRetry: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
LOCATION: {
|
LOCATION: {
|
||||||
isClient: true,
|
Error: HafasNotFoundError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'location/stop not found',
|
message: 'location/stop not found',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H390: {
|
H390: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: departure/arrival station replaced',
|
message: 'journeys search: departure/arrival station replaced',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H410: {
|
H410: {
|
||||||
// todo: or is it a client error?
|
// todo: or is it a client error?
|
||||||
// todo: statusCode?
|
Error: HafasServerError,
|
||||||
isServer: true,
|
message: 'journeys search: incomplete response due to timetable change',
|
||||||
code: SERVER_ERROR,
|
props: {
|
||||||
message: 'journeys search: incomplete response due to timetable change'
|
},
|
||||||
},
|
},
|
||||||
H455: {
|
H455: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: prolonged stop',
|
message: 'journeys search: prolonged stop',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H460: {
|
H460: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: stop(s) passed multiple times',
|
message: 'journeys search: stop(s) passed multiple times',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H500: {
|
H500: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: too many trains, connection is not complete',
|
message: 'journeys search: too many trains, connection is not complete',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H890: {
|
H890: {
|
||||||
isClient: true,
|
Error: HafasNotFoundError,
|
||||||
code: NOT_FOUND,
|
|
||||||
message: 'journeys search unsuccessful',
|
message: 'journeys search unsuccessful',
|
||||||
statusCode: 404
|
props: {
|
||||||
|
shouldRetry: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H891: {
|
H891: {
|
||||||
isClient: true,
|
Error: HafasNotFoundError,
|
||||||
code: NOT_FOUND,
|
|
||||||
message: 'journeys search: no route found, try with an intermediate stations',
|
message: 'journeys search: no route found, try with an intermediate stations',
|
||||||
statusCode: 404
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H892: {
|
H892: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: query too complex, try less intermediate stations',
|
message: 'journeys search: query too complex, try less intermediate stations',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H895: {
|
H895: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: departure & arrival are too near',
|
message: 'journeys search: departure & arrival are too near',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H899: {
|
H899: {
|
||||||
// todo: or is it a client error?
|
// todo: or is it a client error?
|
||||||
// todo: statusCode?
|
Error: HafasServerError,
|
||||||
isServer: true,
|
message: 'journeys search unsuccessful or incomplete due to timetable change',
|
||||||
code: SERVER_ERROR,
|
props: {
|
||||||
message: 'journeys search unsuccessful or incomplete due to timetable change'
|
},
|
||||||
},
|
},
|
||||||
H900: {
|
H900: {
|
||||||
// todo: or is it a client error?
|
// todo: or is it a client error?
|
||||||
// todo: statusCode?
|
Error: HafasServerError,
|
||||||
isServer: true,
|
message: 'journeys search unsuccessful or incomplete due to timetable change',
|
||||||
code: SERVER_ERROR,
|
props: {
|
||||||
message: 'journeys search unsuccessful or incomplete due to timetable change'
|
},
|
||||||
},
|
},
|
||||||
H9220: {
|
H9220: {
|
||||||
isClient: true,
|
Error: HafasNotFoundError,
|
||||||
code: NOT_FOUND,
|
|
||||||
message: 'journeys search: no stations found close to the address',
|
message: 'journeys search: no stations found close to the address',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H9230: {
|
H9230: {
|
||||||
isServer: true,
|
Error: HafasServerError,
|
||||||
code: SERVER_ERROR,
|
|
||||||
message: 'journeys search: an internal error occured',
|
message: 'journeys search: an internal error occured',
|
||||||
statusCode: 500
|
props: {
|
||||||
|
shouldRetry: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H9240: {
|
H9240: {
|
||||||
isClient: true,
|
Error: HafasNotFoundError,
|
||||||
code: NOT_FOUND,
|
|
||||||
message: 'journeys search unsuccessful',
|
message: 'journeys search unsuccessful',
|
||||||
statusCode: 404
|
props: {
|
||||||
|
shouldRetry: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H9250: {
|
H9250: {
|
||||||
isServer: true,
|
Error: HafasServerError,
|
||||||
code: SERVER_ERROR,
|
|
||||||
message: 'journeys search: leg query interrupted',
|
message: 'journeys search: leg query interrupted',
|
||||||
statusCode: 500
|
props: {
|
||||||
|
shouldRetry: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H9260: {
|
H9260: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: unknown departure station',
|
message: 'journeys search: unknown departure station',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H9280: {
|
H9280: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: unknown intermediate station',
|
message: 'journeys search: unknown intermediate station',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H9300: {
|
H9300: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: unknown arrival station',
|
message: 'journeys search: unknown arrival station',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H9320: {
|
H9320: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: the input is incorrect or incomplete',
|
message: 'journeys search: the input is incorrect or incomplete',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H9360: {
|
H9360: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: invalid date/time',
|
message: 'journeys search: invalid date/time',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
H9380: {
|
H9380: {
|
||||||
isClient: true,
|
Error: HafasInvalidRequestError,
|
||||||
code: INVALID_REQUEST,
|
|
||||||
message: 'journeys search: departure/arrival/intermediate station defined more than once',
|
message: 'journeys search: departure/arrival/intermediate station defined more than once',
|
||||||
statusCode: 400
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
SQ001: {
|
SQ001: {
|
||||||
isServer: true,
|
Error: HafasServerError,
|
||||||
code: SERVER_ERROR,
|
|
||||||
message: 'no departures/arrivals data available',
|
message: 'no departures/arrivals data available',
|
||||||
statusCode: 503
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
SQ005: {
|
SQ005: {
|
||||||
isClient: true,
|
Error: HafasNotFoundError,
|
||||||
code: NOT_FOUND,
|
|
||||||
message: 'no trips found',
|
message: 'no trips found',
|
||||||
statusCode: 404
|
props: {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
TI001: {
|
TI001: {
|
||||||
isServer: true,
|
Error: HafasServerError,
|
||||||
code: SERVER_ERROR,
|
|
||||||
message: 'no trip info available',
|
message: 'no trip info available',
|
||||||
statusCode: 503
|
props: {
|
||||||
|
shouldRetry: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -212,5 +275,10 @@ module.exports = {
|
||||||
INVALID_REQUEST,
|
INVALID_REQUEST,
|
||||||
NOT_FOUND,
|
NOT_FOUND,
|
||||||
SERVER_ERROR,
|
SERVER_ERROR,
|
||||||
|
HafasError,
|
||||||
|
HafasAccessDeniedError,
|
||||||
|
HafasInvalidRequestError,
|
||||||
|
HafasNotFoundError,
|
||||||
|
HafasServerError,
|
||||||
byErrorCode,
|
byErrorCode,
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,8 @@ const pick = require('lodash/pick')
|
||||||
const {stringify} = require('qs')
|
const {stringify} = require('qs')
|
||||||
const {Request, fetch} = require('cross-fetch')
|
const {Request, fetch} = require('cross-fetch')
|
||||||
const {parse: parseContentType} = require('content-type')
|
const {parse: parseContentType} = require('content-type')
|
||||||
const {byErrorCode} = require('./errors')
|
const {HafasError} = require('./errors')
|
||||||
|
const checkIfResponseIsOk = require('./check-if-res-is-ok')
|
||||||
|
|
||||||
const proxyAddress = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || null
|
const proxyAddress = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || null
|
||||||
const localAddresses = process.env.LOCAL_ADDRESS || null
|
const localAddresses = process.env.LOCAL_ADDRESS || null
|
||||||
|
@ -127,15 +128,14 @@ const request = async (ctx, userAgent, reqData) => {
|
||||||
|
|
||||||
const res = await fetch(url, req)
|
const res = await fetch(url, req)
|
||||||
|
|
||||||
// Async stack traces are not supported everywhere yet, so we create our own.
|
|
||||||
const errProps = {
|
const errProps = {
|
||||||
isHafasError: true, // todo: rename to `isHafasClientError`
|
request: fetchReq,
|
||||||
statusCode: res.status,
|
response: res,
|
||||||
request: req.body, // todo [breaking]: change to fetchReq
|
url,
|
||||||
url: url,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
// todo [breaking]: make this a FetchError or a HafasClientError?
|
||||||
const err = new Error(res.statusText)
|
const err = new Error(res.statusText)
|
||||||
Object.assign(err, errProps)
|
Object.assign(err, errProps)
|
||||||
throw err
|
throw err
|
||||||
|
@ -145,9 +145,7 @@ const request = async (ctx, userAgent, reqData) => {
|
||||||
if (cType) {
|
if (cType) {
|
||||||
const {type} = parseContentType(cType)
|
const {type} = parseContentType(cType)
|
||||||
if (type !== 'application/json') {
|
if (type !== 'application/json') {
|
||||||
const err = new Error('invalid response content-type: ' + cType)
|
throw new HafasError('invalid/unsupported response content-type: ' + cType, null, errProps)
|
||||||
err.response = res
|
|
||||||
throw err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,30 +153,10 @@ const request = async (ctx, userAgent, reqData) => {
|
||||||
profile.logResponse(ctx, res, body, reqId)
|
profile.logResponse(ctx, res, body, reqId)
|
||||||
|
|
||||||
const b = JSON.parse(body)
|
const b = JSON.parse(body)
|
||||||
if (b.err) errProps.hafasErrorCode = b.err
|
checkIfResponseIsOk({
|
||||||
if (b.errTxt) errProps.hafasErrorMessage = b.errTxt
|
body: b,
|
||||||
if (b.id) errProps.responseId = b.id
|
errProps,
|
||||||
if (b.err && b.err !== 'OK') {
|
})
|
||||||
const err = new Error(b.errTxt || b.err)
|
|
||||||
Object.assign(err, errProps)
|
|
||||||
if (b.err in byErrorCode) {
|
|
||||||
Object.assign(err, byErrorCode[b.err])
|
|
||||||
}
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
if (!b.svcResL || !b.svcResL[0]) {
|
|
||||||
const err = new Error('invalid/unsupported response structure')
|
|
||||||
Object.assign(err, errProps)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
if (b.svcResL[0].err !== 'OK') {
|
|
||||||
const err = new Error(b.svcResL[0].errTxt || b.svcResL[0].err)
|
|
||||||
Object.assign(err, errProps)
|
|
||||||
if (b.svcResL[0].err in byErrorCode) {
|
|
||||||
Object.assign(err, byErrorCode[b.svcResL[0].err])
|
|
||||||
}
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
|
|
||||||
const svcRes = b.svcResL[0].res
|
const svcRes = b.svcResL[0].res
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"test-unit": "tap test/*.js test/format/*.js test/parse/*.js",
|
"test-unit": "tap test/lib/*.js test/*.js test/format/*.js test/parse/*.js",
|
||||||
"test-integration": "VCR_MODE=playback tap test/e2e/*.js",
|
"test-integration": "VCR_MODE=playback tap test/e2e/*.js",
|
||||||
"test-integration:record": "VCR_MODE=record tap -t60 -j16 test/e2e/*.js",
|
"test-integration:record": "VCR_MODE=record tap -t60 -j16 test/e2e/*.js",
|
||||||
"test-e2e": "VCR_OFF=true tap -t60 -j16 test/e2e/*.js",
|
"test-e2e": "VCR_OFF=true tap -t60 -j16 test/e2e/*.js",
|
||||||
|
|
28
test/fixtures/error-h9360.json
vendored
Normal file
28
test/fixtures/error-h9360.json
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"ver": "1.44",
|
||||||
|
"ext": "BVG.1",
|
||||||
|
"lang": "deu",
|
||||||
|
"id": "3xmggksuiukkx6wx",
|
||||||
|
"err": "OK",
|
||||||
|
"graph": {
|
||||||
|
"id": "standard",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"subGraph": {
|
||||||
|
"id": "global",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"id": "standard",
|
||||||
|
"index": 0,
|
||||||
|
"type": "WGS84"
|
||||||
|
},
|
||||||
|
"svcResL": [
|
||||||
|
{
|
||||||
|
"meth": "StationBoard",
|
||||||
|
"err": "H9360",
|
||||||
|
"errTxt": "HAFAS Kernel: Date outside of the timetable period.",
|
||||||
|
"errTxtOut": "Fehler bei der Datumseingabe oder Datum außerhalb der Fahrplanperiode (01.05.2022 - 10.12.2022)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
28
test/fixtures/error-location.json
vendored
Normal file
28
test/fixtures/error-location.json
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"ver": "1.44",
|
||||||
|
"ext": "BVG.1",
|
||||||
|
"lang": "deu",
|
||||||
|
"id": "mmmxakseiuk9g6wg",
|
||||||
|
"err": "OK",
|
||||||
|
"graph": {
|
||||||
|
"id": "standard",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"subGraph": {
|
||||||
|
"id": "global",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"id": "standard",
|
||||||
|
"index": 0,
|
||||||
|
"type": "WGS84"
|
||||||
|
},
|
||||||
|
"svcResL": [
|
||||||
|
{
|
||||||
|
"meth": "StationBoard",
|
||||||
|
"err": "LOCATION",
|
||||||
|
"errTxt": "HCI Service: location missing or invalid",
|
||||||
|
"errTxtOut": "Während der Suche ist ein interner Fehler aufgetreten"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
109
test/lib/check-if-res-is-ok.js
Normal file
109
test/lib/check-if-res-is-ok.js
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const tap = require('tap')
|
||||||
|
const checkIfResIsOk = require('../../lib/check-if-res-is-ok')
|
||||||
|
const {
|
||||||
|
INVALID_REQUEST,
|
||||||
|
NOT_FOUND,
|
||||||
|
HafasError,
|
||||||
|
HafasInvalidRequestError,
|
||||||
|
HafasNotFoundError,
|
||||||
|
} = require('../../lib/errors')
|
||||||
|
|
||||||
|
const resParameter = require('../fixtures/error-parameter.json')
|
||||||
|
const resNoMatch = require('../fixtures/error-no-match.json')
|
||||||
|
const resH9360 = require('../fixtures/error-h9360.json')
|
||||||
|
const resLocation = require('../fixtures/error-location.json')
|
||||||
|
|
||||||
|
const secret = Symbol('secret')
|
||||||
|
|
||||||
|
tap.test('checkIfResponseIsOk properly throws HAFAS "H9360" errors', (t) => {
|
||||||
|
try {
|
||||||
|
checkIfResIsOk({
|
||||||
|
body: resH9360,
|
||||||
|
errProps: {secret},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
t.ok(err)
|
||||||
|
|
||||||
|
t.ok(err instanceof HafasError)
|
||||||
|
t.equal(err.isHafasError, true)
|
||||||
|
t.equal(err.message.slice(0, 7), 'H9360: ')
|
||||||
|
t.ok(err.message.length > 7)
|
||||||
|
|
||||||
|
t.ok(err instanceof HafasInvalidRequestError)
|
||||||
|
t.equal(err.isCausedByServer, false)
|
||||||
|
t.equal(err.code, INVALID_REQUEST)
|
||||||
|
t.equal(err.hafasCode, 'H9360')
|
||||||
|
|
||||||
|
t.equal(err.hafasResponseId, resH9360.id)
|
||||||
|
t.equal(err.hafasMessage, 'HAFAS Kernel: Date outside of the timetable period.')
|
||||||
|
t.equal(err.hafasDescription, 'Fehler bei der Datumseingabe oder Datum außerhalb der Fahrplanperiode (01.05.2022 - 10.12.2022)')
|
||||||
|
t.equal(err.secret, secret)
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tap.test('checkIfResponseIsOk properly throws HAFAS "LOCATION" errors', (t) => {
|
||||||
|
try {
|
||||||
|
checkIfResIsOk({
|
||||||
|
body: resLocation,
|
||||||
|
errProps: {secret},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
t.ok(err)
|
||||||
|
|
||||||
|
t.ok(err instanceof HafasError)
|
||||||
|
t.equal(err.isHafasError, true)
|
||||||
|
t.equal(err.message.slice(0, 10), 'LOCATION: ')
|
||||||
|
t.ok(err.message.length > 10)
|
||||||
|
|
||||||
|
t.ok(err instanceof HafasNotFoundError)
|
||||||
|
t.equal(err.isCausedByServer, false)
|
||||||
|
t.equal(err.code, NOT_FOUND)
|
||||||
|
t.equal(err.hafasCode, 'LOCATION')
|
||||||
|
|
||||||
|
t.equal(err.hafasResponseId, resLocation.id)
|
||||||
|
t.equal(err.hafasMessage, 'HCI Service: location missing or invalid')
|
||||||
|
t.equal(err.hafasDescription, 'Während der Suche ist ein interner Fehler aufgetreten')
|
||||||
|
t.equal(err.secret, secret)
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tap.test('checkIfResponseIsOk properly parses an unknown HAFAS errors', (t) => {
|
||||||
|
const body = {
|
||||||
|
ver: '1.42',
|
||||||
|
id: '1234567890',
|
||||||
|
err: 'FOO',
|
||||||
|
errTxt: 'random errTxt',
|
||||||
|
errTxtOut: 'even more random errTxtOut',
|
||||||
|
svcResL: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
checkIfResIsOk({
|
||||||
|
body,
|
||||||
|
errProps: {secret},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
t.ok(err)
|
||||||
|
|
||||||
|
t.ok(err instanceof HafasError)
|
||||||
|
t.equal(err.isHafasError, true)
|
||||||
|
t.equal(err.message, `${body.err}: ${body.errTxt}`)
|
||||||
|
|
||||||
|
t.equal(err.isCausedByServer, false)
|
||||||
|
t.equal(err.code, null)
|
||||||
|
t.equal(err.hafasCode, body.err)
|
||||||
|
|
||||||
|
t.equal(err.hafasResponseId, body.id)
|
||||||
|
t.equal(err.hafasMessage, body.errTxt)
|
||||||
|
t.equal(err.hafasDescription, body.errTxtOut)
|
||||||
|
t.equal(err.secret, secret)
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
}
|
||||||
|
})
|
Loading…
Add table
Reference in a new issue