mirror of
				https://github.com/public-transport/db-vendo-client.git
				synced 2025-11-04 10:06:32 +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.
 | 
			
		||||
 | 
			
		||||
## 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
 | 
			
		||||
 | 
			
		||||
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 {INVALID_REQUEST} = require('./lib/errors')
 | 
			
		||||
const sliceLeg = require('./lib/slice-leg')
 | 
			
		||||
const {HafasError} = require('./lib/errors')
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
		if (!Array.isArray(res.outConL) || !res.outConL[0]) {
 | 
			
		||||
			const err = new Error('invalid response')
 | 
			
		||||
			// technically this is not a HAFAS error
 | 
			
		||||
			// todo: find a different flag with decent DX
 | 
			
		||||
			err.isHafasError = true
 | 
			
		||||
			throw err
 | 
			
		||||
			throw new HafasError('invalid response, expected outConL[0]', null, {})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
		if (!res || !Array.isArray(res.locL) || !res.locL[0]) {
 | 
			
		||||
			// todo: proper stack trace?
 | 
			
		||||
			// todo: DRY with lib/request.js
 | 
			
		||||
			const err = new Error('response has no stop')
 | 
			
		||||
			// technically this is not a HAFAS error
 | 
			
		||||
			// todo: find a different flag with decent DX
 | 
			
		||||
			err.isHafasError = true
 | 
			
		||||
			err.code = INVALID_REQUEST
 | 
			
		||||
			throw err
 | 
			
		||||
			throw new HafasError('invalid response, expected locL[0]', null, {
 | 
			
		||||
				// This problem occurs on invalid input. 🙄
 | 
			
		||||
				code: INVALID_REQUEST,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
		if (!Array.isArray(res.posL)) {
 | 
			
		||||
			const err = new Error('invalid response')
 | 
			
		||||
			err.shouldRetry = true
 | 
			
		||||
			throw err
 | 
			
		||||
			throw new HafasError('invalid response, expected posL[0]', null, {
 | 
			
		||||
				shouldRetry: true,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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 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
 | 
			
		||||
// todo:
 | 
			
		||||
// `code: 'METHOD_NA', message: 'HCI Service: service method disabled'`
 | 
			
		||||
// "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), {
 | 
			
		||||
	H_UNKNOWN: {
 | 
			
		||||
		isServer: false,
 | 
			
		||||
		code: SERVER_ERROR,
 | 
			
		||||
		Error: HafasError,
 | 
			
		||||
		message: 'unknown internal error',
 | 
			
		||||
		statusCode: 500,
 | 
			
		||||
		props: {
 | 
			
		||||
			shouldRetry: true,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	AUTH: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: ACCESS_DENIED,
 | 
			
		||||
		Error: HafasAccessDeniedError,
 | 
			
		||||
		message: 'invalid or missing authentication data',
 | 
			
		||||
		statusCode: 401
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	R0001: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'unknown method',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	R0002: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'invalid or missing request parameters',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	R0007: {
 | 
			
		||||
		isServer: true,
 | 
			
		||||
		code: SERVER_ERROR,
 | 
			
		||||
		Error: HafasServerError,
 | 
			
		||||
		message: 'internal communication error',
 | 
			
		||||
		statusCode: 500
 | 
			
		||||
		props: {
 | 
			
		||||
			shouldRetry: true,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	R5000: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: ACCESS_DENIED,
 | 
			
		||||
		Error: HafasAccessDeniedError,
 | 
			
		||||
		message: 'access denied',
 | 
			
		||||
		statusCode: 401
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	S1: {
 | 
			
		||||
		isServer: true,
 | 
			
		||||
		code: SERVER_ERROR,
 | 
			
		||||
		Error: HafasServerError,
 | 
			
		||||
		message: 'journeys search: a connection to the backend server couldn\'t be established',
 | 
			
		||||
		statusCode: 503
 | 
			
		||||
		props: {
 | 
			
		||||
			shouldRetry: true,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	LOCATION: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasNotFoundError,
 | 
			
		||||
		message: 'location/stop not found',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H390: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: departure/arrival station replaced',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H410: {
 | 
			
		||||
		// todo: or is it a client error?
 | 
			
		||||
		// todo: statusCode?
 | 
			
		||||
		isServer: true,
 | 
			
		||||
		code: SERVER_ERROR,
 | 
			
		||||
		message: 'journeys search: incomplete response due to timetable change'
 | 
			
		||||
		Error: HafasServerError,
 | 
			
		||||
		message: 'journeys search: incomplete response due to timetable change',
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H455: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: prolonged stop',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H460: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: stop(s) passed multiple times',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H500: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: too many trains, connection is not complete',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H890: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: NOT_FOUND,
 | 
			
		||||
		Error: HafasNotFoundError,
 | 
			
		||||
		message: 'journeys search unsuccessful',
 | 
			
		||||
		statusCode: 404
 | 
			
		||||
		props: {
 | 
			
		||||
			shouldRetry: true,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H891: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: NOT_FOUND,
 | 
			
		||||
		Error: HafasNotFoundError,
 | 
			
		||||
		message: 'journeys search: no route found, try with an intermediate stations',
 | 
			
		||||
		statusCode: 404
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H892: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: query too complex, try less intermediate stations',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H895: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: departure & arrival are too near',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H899: {
 | 
			
		||||
		// todo: or is it a client error?
 | 
			
		||||
		// todo: statusCode?
 | 
			
		||||
		isServer: true,
 | 
			
		||||
		code: SERVER_ERROR,
 | 
			
		||||
		message: 'journeys search unsuccessful or incomplete due to timetable change'
 | 
			
		||||
		Error: HafasServerError,
 | 
			
		||||
		message: 'journeys search unsuccessful or incomplete due to timetable change',
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H900: {
 | 
			
		||||
		// todo: or is it a client error?
 | 
			
		||||
		// todo: statusCode?
 | 
			
		||||
		isServer: true,
 | 
			
		||||
		code: SERVER_ERROR,
 | 
			
		||||
		message: 'journeys search unsuccessful or incomplete due to timetable change'
 | 
			
		||||
		Error: HafasServerError,
 | 
			
		||||
		message: 'journeys search unsuccessful or incomplete due to timetable change',
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H9220: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: NOT_FOUND,
 | 
			
		||||
		Error: HafasNotFoundError,
 | 
			
		||||
		message: 'journeys search: no stations found close to the address',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H9230: {
 | 
			
		||||
		isServer: true,
 | 
			
		||||
		code: SERVER_ERROR,
 | 
			
		||||
		Error: HafasServerError,
 | 
			
		||||
		message: 'journeys search: an internal error occured',
 | 
			
		||||
		statusCode: 500
 | 
			
		||||
		props: {
 | 
			
		||||
			shouldRetry: true,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H9240: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: NOT_FOUND,
 | 
			
		||||
		Error: HafasNotFoundError,
 | 
			
		||||
		message: 'journeys search unsuccessful',
 | 
			
		||||
		statusCode: 404
 | 
			
		||||
		props: {
 | 
			
		||||
			shouldRetry: true,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H9250: {
 | 
			
		||||
		isServer: true,
 | 
			
		||||
		code: SERVER_ERROR,
 | 
			
		||||
		Error: HafasServerError,
 | 
			
		||||
		message: 'journeys search: leg query interrupted',
 | 
			
		||||
		statusCode: 500
 | 
			
		||||
		props: {
 | 
			
		||||
			shouldRetry: true,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H9260: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: unknown departure station',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H9280: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: unknown intermediate station',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H9300: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: unknown arrival station',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H9320: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: the input is incorrect or incomplete',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H9360: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: invalid date/time',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	H9380: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: INVALID_REQUEST,
 | 
			
		||||
		Error: HafasInvalidRequestError,
 | 
			
		||||
		message: 'journeys search: departure/arrival/intermediate station defined more than once',
 | 
			
		||||
		statusCode: 400
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	SQ001: {
 | 
			
		||||
		isServer: true,
 | 
			
		||||
		code: SERVER_ERROR,
 | 
			
		||||
		Error: HafasServerError,
 | 
			
		||||
		message: 'no departures/arrivals data available',
 | 
			
		||||
		statusCode: 503
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	SQ005: {
 | 
			
		||||
		isClient: true,
 | 
			
		||||
		code: NOT_FOUND,
 | 
			
		||||
		Error: HafasNotFoundError,
 | 
			
		||||
		message: 'no trips found',
 | 
			
		||||
		statusCode: 404
 | 
			
		||||
		props: {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	TI001: {
 | 
			
		||||
		isServer: true,
 | 
			
		||||
		code: SERVER_ERROR,
 | 
			
		||||
		Error: HafasServerError,
 | 
			
		||||
		message: 'no trip info available',
 | 
			
		||||
		statusCode: 503
 | 
			
		||||
		props: {
 | 
			
		||||
			shouldRetry: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -212,5 +275,10 @@ module.exports = {
 | 
			
		|||
	INVALID_REQUEST,
 | 
			
		||||
	NOT_FOUND,
 | 
			
		||||
	SERVER_ERROR,
 | 
			
		||||
	HafasError,
 | 
			
		||||
	HafasAccessDeniedError,
 | 
			
		||||
	HafasInvalidRequestError,
 | 
			
		||||
	HafasNotFoundError,
 | 
			
		||||
	HafasServerError,
 | 
			
		||||
	byErrorCode,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,8 @@ const pick = require('lodash/pick')
 | 
			
		|||
const {stringify} = require('qs')
 | 
			
		||||
const {Request, fetch} = require('cross-fetch')
 | 
			
		||||
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 localAddresses = process.env.LOCAL_ADDRESS || null
 | 
			
		||||
| 
						 | 
				
			
			@ -127,15 +128,14 @@ const request = async (ctx, userAgent, reqData) => {
 | 
			
		|||
 | 
			
		||||
	const res = await fetch(url, req)
 | 
			
		||||
 | 
			
		||||
	// Async stack traces are not supported everywhere yet, so we create our own.
 | 
			
		||||
	const errProps = {
 | 
			
		||||
		isHafasError: true, // todo: rename to `isHafasClientError`
 | 
			
		||||
		statusCode: res.status,
 | 
			
		||||
		request: req.body, // todo [breaking]: change to fetchReq
 | 
			
		||||
		url: url,
 | 
			
		||||
		request: fetchReq,
 | 
			
		||||
		response: res,
 | 
			
		||||
		url,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!res.ok) {
 | 
			
		||||
		// todo [breaking]: make this a FetchError or a HafasClientError?
 | 
			
		||||
		const err = new Error(res.statusText)
 | 
			
		||||
		Object.assign(err, errProps)
 | 
			
		||||
		throw err
 | 
			
		||||
| 
						 | 
				
			
			@ -145,9 +145,7 @@ const request = async (ctx, userAgent, reqData) => {
 | 
			
		|||
	if (cType) {
 | 
			
		||||
		const {type} = parseContentType(cType)
 | 
			
		||||
		if (type !== 'application/json') {
 | 
			
		||||
			const err = new Error('invalid response content-type: ' + cType)
 | 
			
		||||
			err.response = res
 | 
			
		||||
			throw err
 | 
			
		||||
			throw new HafasError('invalid/unsupported response content-type: ' + cType, null, errProps)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -155,30 +153,10 @@ const request = async (ctx, userAgent, reqData) => {
 | 
			
		|||
	profile.logResponse(ctx, res, body, reqId)
 | 
			
		||||
 | 
			
		||||
	const b = JSON.parse(body)
 | 
			
		||||
	if (b.err) errProps.hafasErrorCode = b.err
 | 
			
		||||
	if (b.errTxt) errProps.hafasErrorMessage = b.errTxt
 | 
			
		||||
	if (b.id) errProps.responseId = b.id
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
	checkIfResponseIsOk({
 | 
			
		||||
		body: b,
 | 
			
		||||
		errProps,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	const svcRes = b.svcResL[0].res
 | 
			
		||||
	return {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -70,7 +70,7 @@
 | 
			
		|||
	},
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"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:record": "VCR_MODE=record 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