-
Notifications
You must be signed in to change notification settings - Fork 6
Feature/api_client #370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Feature/api_client #370
Changes from all commits
Commits
Show all changes
77 commits
Select commit
Hold shift + click to select a range
4a0a951
feat: add centralized http module containing generic request method a…
93b41d1
http: implement helper methods
e44f489
refactor: remove ties to other parts of the app, always throw on error
3503b8a
update request method signature
f707d5c
refactor: seperate generic http logic and client specific request mec…
8d0f01c
fix: namings and method bindings
fff8e4f
refactor: use ts
b159d6c
refactor: move code into own folder
dc05048
refactor: use typescript, move code to own module
30aa878
start adding tests
21e87bc
Merge remote-tracking branch 'origin/feature/centralize_api_logic' in…
39fa7eb
use ky-universal in tests
94c3584
add timeout option to request config
2565295
stop working on this for now
6b8b5b9
rewrite
32dd5aa
refactor request method signature: split ky and callback options
de899d9
fix: options merging
99c512d
stop working for today
3fd0768
add todo item
60140e1
fix(OverviewMapState.unit.test.js): HTPPError not exported from ky
b58b842
add proper types for fetch
5a714e9
improve(UserState): add token selector
6bf0371
further implementation
ceae825
further implement feature
4395a6a
update stackoverflow link
0c9039b
chore(jest setup): require dotenv
3f18d0b
refactor: rename main file to apiService
dbe66a5
test: correctly test post req
51203fc
refactor(apiService): clean up
53c3c75
continue working on error handling
42c3797
add todo-test
e4a390f
refactor: rename BodyType to ResponseBodyType
6946c90
improve: set appropriate request headers for post requests fetching json
ca35704
refactor(api.unit.test): restructure suites
f5198af
refactor signature of generic request handler to take an object as se…
faa9dea
further shape things
s-pic 0ba8a94
improve(auth header test): actually test headers
s-pic 2f996dd
refactor(type): prettify
s-pic a5c38d4
fix parsing of error response body, make all current tests pass
s-pic 47e380e
chore: add missing dotenv devDependency
s-pic 0ddb665
improve(apiService): in error response parsing do not rely on respons…
s-pic 7d8b2e0
refactor(apiService): cleanup
s-pic 49cb4e6
feat(api.unit.test): test patch shorthand
s-pic 09fb77c
add todo test
s-pic e970480
fix(api.unit.test.js): syntax error in todo test
s-pic 5e2ac67
refactor(apiService): cleanup
s-pic 86064fd
merge develop
s-pic d4fde6e
Merge remote-tracking branch 'origin/feature/centralize_api_logic' in…
s-pic de729e5
Revert "merge develop"
s-pic c36142a
refactor(api.unit.test.js): cleanup
s-pic 6be8608
Merge branch 'develop' into feature/centralize_api_logic
s-pic 1b0d31c
Fix issue with react-piwik-router in Jest env
cafca 5165dc3
refactor(package.json): remove dotenv from devDependencies
s-pic 9a9fe0a
refactor(utils.js): remove unused isFunction util
s-pic cd1ddf0
refactor(api.unit.test.js): rename constants
s-pic 7050ffd
refactor(api.unit.test.js): add comment
s-pic 48f6b8a
refactor(apiService): remove unnecessary emptyFunc typing
s-pic 09bd812
refactor(apiService): prettify
s-pic 0fb4e21
refactor(apiService): split into multiple files bundled in index.ts
s-pic 4c463a7
refactor(errorHandling): default export main method to follow lint rules
s-pic 1305a42
improve(errorHandling): if the API answers with a JSON error not hold…
s-pic 655b210
Add review changes
cafca 84515fb
Simplify constants and helper function
cafca 8f19a5f
Add types, logging
cafca 002b541
Update syntax and add onSlowResponse cb
cafca ff7de26
Merge branch 'develop' into feature/centralize_api_logic
cafca a7f5dea
Add docs, rename symbols and modules
cafca 8931e85
refactor(custom errors): remove Error.captureStackTrace() calls
s-pic c61e3fa
docs(custom errors)
s-pic 6260769
test(api): factor out utils
s-pic b10f73d
test(api): explicitly test for read- and connection timeout errors
s-pic ef42349
test(api): test for network errors in a basic (bad) way
s-pic a100319
test(api): simplify test
s-pic b527aa8
More concise syntax
cafca 7141027
Set ApiError.message to null if parsing message fails
cafca 82425f1
Revert module name
cafca 69c1863
Update test
cafca File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| // use ky-universal instead of ky, which is only meant to be used in the browser | ||
| module.exports = require('ky-universal'); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // TODO: use functions instead of classes to respect eslint, see https://dev.to/damxipo/custom-exceptions-with-js-3aoc | ||
|
|
||
| /** | ||
| * The api has answered with an error description. | ||
| */ | ||
| class ApiError extends Error { | ||
| public code: number; | ||
|
|
||
| constructor(message, statusCode = 500) { | ||
| super(message); | ||
| this.name = 'ApiError'; | ||
| this.code = statusCode; | ||
| Object.setPrototypeOf(this, ApiError.prototype); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Indicates a Read Timeout (it took to long after the last byte has been sent) | ||
| * or a Connection Timeout (the server never responded) | ||
| */ | ||
| class TimeoutError extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = 'TimeoutError'; | ||
| Object.setPrototypeOf(this, TimeoutError.prototype); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Communication with the api has failed, usually if | ||
| * "when a network error is encountered or CORS is misconfigured on the server-side", | ||
| * see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch | ||
| */ | ||
| class NetworkError extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = 'NetworkError'; | ||
| Object.setPrototypeOf(this, NetworkError.prototype); | ||
| } | ||
| } | ||
|
|
||
| export { ApiError, TimeoutError, NetworkError }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import request from './request'; | ||
| import { get, post, patch } from './shorthands'; | ||
| import { ApiError, TimeoutError, NetworkError } from './errors'; | ||
|
|
||
| export default { | ||
| request, | ||
| get, | ||
| post, | ||
| patch, | ||
| ApiError, | ||
| TimeoutError, | ||
| NetworkError | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import ky from 'ky-universal'; | ||
| import debug from 'debug'; | ||
| import { ApiError, NetworkError, TimeoutError } from './errors'; | ||
| import { FMCError, JSONValue, JSONObject } from './types'; | ||
|
|
||
| const log = debug('fmc:api:mapError'); | ||
|
|
||
| /** | ||
| * Translate errors reported by ky/fetch to a set of custom exceptions | ||
| * which we can later on use to make decisions on how to handle specific errors. | ||
| */ | ||
| export default async function mapError(e: FMCError): Promise<FMCError> { | ||
| let errorMessage: string; | ||
| let statusCode: number; | ||
|
|
||
| // handle network errors (misspelled URL, flaky or no network or CORS problems) | ||
| if (e.message === 'Failed to fetch') { | ||
| return new NetworkError(e.message); | ||
| } | ||
|
|
||
| // handle all other errors | ||
| switch (e.constructor) { | ||
| case ky.HTTPError: // a non 2xx error code was found | ||
| errorMessage = await parseErrorResponse(e.response); | ||
| statusCode = e.response.status; | ||
| return new ApiError(errorMessage, statusCode); | ||
| case ky.TimeoutError: | ||
| return new TimeoutError(e.message); | ||
| default: | ||
| // any other error, just forward it | ||
| return e; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Try parsing an error response as JSON or fallback to returning as text | ||
| * | ||
| * If a JSON error response contains a field `detail` of string type, its | ||
| * content is returned as raw text | ||
| * | ||
| * @param errorResponse API return value | ||
| */ | ||
| async function parseErrorResponse( | ||
| errorResponse: Response | ||
| ): Promise<string | null> { | ||
| let errorJson: JSONValue; | ||
| let errorText: string; | ||
|
|
||
| // try parse error as text and then as JSON to not rely on content-type | ||
| // definitions response headers | ||
| try { | ||
| errorText = await errorResponse.text(); | ||
| } catch (e) { | ||
| log(e); | ||
| throw new TypeError('Only JSON and text error responses can be handled'); | ||
| } | ||
|
|
||
| try { | ||
| errorJson = JSON.parse(errorText); | ||
| } catch (e) { | ||
| log('found error response encoded as raw text'); | ||
| return errorText; | ||
| } | ||
|
|
||
| if (typeof (errorJson as JSONObject)?.detail === 'string') { | ||
| log('found `detail` field in json response'); | ||
| return (errorJson as JSONObject)?.detail.toString(); | ||
| } | ||
| log('found json-encoded error response', errorJson); | ||
| return null; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| /** | ||
| * Facilitates http request creation by | ||
| * - offering helper methods with simplified interfaces | ||
| * - allowing to hook into before/after a request is made or if an error occurs | ||
| * - doing preparatory work for network error request handling | ||
| */ | ||
| import ky from 'ky-universal'; | ||
| import debug from 'debug'; | ||
| import config from '~/config'; | ||
| import store from '~/store'; | ||
| import { RequestOptions } from './types'; | ||
| import { selectors as UserStateSelectors } from '~/pages/User/UserState'; | ||
| import mapError from './mapError'; | ||
|
|
||
| const log = debug('fmc:api:request()'); | ||
|
|
||
| // setup ky | ||
|
|
||
| const configuredKy = ky.create({ | ||
| prefixUrl: config.apiUrl, | ||
| hooks: { | ||
| beforeRequest: [ | ||
| // Set authorization headers | ||
| (req: Request) => { | ||
| const stateRoot = store.getState(); | ||
| const token = UserStateSelectors.getToken(stateRoot); | ||
| if (token) { | ||
| req.headers.set('Authorization', `JWT ${token}`); | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| }); | ||
|
|
||
| // Generic Request Handler | ||
|
|
||
| const defaultOptions: RequestOptions = { | ||
| accept: 'json', | ||
| method: 'get', | ||
| timeout: 30 * 1000, | ||
| slowResponseTimeout: 5 * 1000 | ||
| }; | ||
|
|
||
| /** | ||
| * Send requests with convenience callbacks, FMC auth header, raising fmcErrors | ||
|
s-pic marked this conversation as resolved.
|
||
| * | ||
| * @example | ||
| * // Handle server error responses | ||
| * try { | ||
| * const response = await api.request('https://fixmyberlin.de/api/v1/') | ||
| * } catch (err) { | ||
| * if(err instanceof api.ApiError) { | ||
| * console.log(err.code) | ||
| * // 500 | ||
| * console.log(err.message) | ||
| * // parsed from json or text response body | ||
| * } | ||
| * throw(error) | ||
| * } | ||
| * | ||
| * @example | ||
| * // Use callbacks | ||
| * await api.request(url, { | ||
| * onSubmit: () => dispatch(setLoading(true)), | ||
| * onFinish: () => dispatch(setLoading(false)), | ||
| * onSlowRequest: () => dispatch(setSlowRequestNotice(true)) | ||
| * }) | ||
| * | ||
| * @param route URL to request | ||
| * @param options object with options, extending ky's options | ||
| * @param options.accept expected response body type, either `string` or `json` | ||
| * @param options.onSubmit function to call before making request | ||
| * @param options.onFinish function to call after request finishes (successful or not) | ||
| * @param options.onSlowResponse function to call when request is slow | ||
| * @param options.slowResponseTimeout value in milliseconds to wait before | ||
| * calling onSlowResponse, defaults to 5000 | ||
| */ | ||
| export default async function request( | ||
| route: string, | ||
| options: RequestOptions = defaultOptions | ||
| ): Promise<Response> { | ||
| let response; | ||
|
|
||
| const { | ||
| accept = defaultOptions.accept, | ||
| slowResponseTimeout = defaultOptions.slowResponseTimeout, | ||
| onSubmit, | ||
| onFinish, | ||
|
s-pic marked this conversation as resolved.
|
||
| onSlowResponse, | ||
| ...kyOptions | ||
| } = options; | ||
|
|
||
| const timeout = setTimeout(() => { | ||
| if (onSlowResponse) { | ||
| log('calling slow request handler'); | ||
| onSlowResponse(slowResponseTimeout); | ||
| } | ||
| }, slowResponseTimeout); | ||
|
|
||
| if (onSubmit) onSubmit(); | ||
| try { | ||
| log('sending request', { route, options, accept }); | ||
| response = await configuredKy(route, kyOptions)[accept](); | ||
| } catch (e) { | ||
| log('mapping error type', { error: e }); | ||
| throw await mapError(e); | ||
| } finally { | ||
| log('finished request'); | ||
| if (onFinish) onFinish(); | ||
| clearTimeout(timeout); | ||
| } | ||
| return response; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.