Skip to content

Commit e3e48fc

Browse files
committed
Merge branch 'master' into jeff-tests
2 parents 19af67d + d7efd20 commit e3e48fc

4 files changed

Lines changed: 111 additions & 52 deletions

File tree

services/http.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
export type FetchConfig = Omit<RequestInit, 'headers'> & {
2+
headers?: Record<string, string>;
3+
};
4+
5+
export type HttpBody = BodyInit | object;
6+
17
export class BaseHttpClientError extends Error {
28
response: Response;
39

@@ -26,23 +32,23 @@ export abstract class BaseHttpClient {
2632
return this._baseUrl + rest
2733
}
2834

29-
_get(url: string, config?: object): Promise<Response> {
35+
_get(url: string, config?: FetchConfig): Promise<Response> {
3036
return this._send(url, 'GET', undefined, config);
3137
}
3238

33-
_post(url: string, body?: any, config?: object): Promise<Response> {
39+
_post(url: string, body?: HttpBody, config?: FetchConfig): Promise<Response> {
3440
return this._send(url, 'POST', body, config);
3541
}
3642

37-
_put(url: string, body?: any, config?: object): Promise<Response> {
43+
_put(url: string, body?: HttpBody, config?: FetchConfig): Promise<Response> {
3844
return this._send(url, 'PUT', body, config);
3945
}
4046

41-
_patch(url: string, body?: any, config?: object): Promise<Response> {
47+
_patch(url: string, body?: HttpBody, config?: FetchConfig): Promise<Response> {
4248
return this._send(url, 'PATCH', body, config);
4349
}
4450

45-
_delete(url: string, config?: object): Promise<Response> {
51+
_delete(url: string, config?: FetchConfig): Promise<Response> {
4652
return this._send(url, 'DELETE', undefined, config);
4753
}
4854

@@ -63,22 +69,46 @@ export abstract class BaseHttpClient {
6369
return response;
6470
}
6571

66-
async _send(url: string, method: string, body?: any, config?: object): Promise<Response> {
72+
async _send(
73+
url: string,
74+
method: string,
75+
body?: HttpBody,
76+
config?: FetchConfig,
77+
): Promise<Response> {
78+
const { headers: configHeaders, ...restConfig } = config ?? {};
79+
const mergedHeaders: Record<string, string> = {
80+
...this._requestHeaders,
81+
...configHeaders,
82+
};
6783
const requestOptions = {
6884
method,
69-
body,
70-
headers: this._requestHeaders,
85+
body: undefined as BodyInit | undefined,
86+
headers: mergedHeaders,
7187
signal: this._abortSignal,
72-
...config
88+
...restConfig,
7389
};
7490

75-
if (requestOptions.headers['Content-Type'] === 'application/json'
76-
&& !(body instanceof FormData)
77-
&& typeof body !== 'undefined'
78-
&& body !== null
79-
) {
91+
// Let the browser set Content-Type (and multipart boundary) for FormData:
92+
if (body instanceof FormData) {
93+
delete mergedHeaders['Content-Type'];
94+
requestOptions.body = body;
95+
}
96+
else if (body !== null && body !== undefined && (
97+
mergedHeaders['Content-Type'] === 'application/json'
98+
|| (
99+
typeof body === 'object'
100+
&& !(body instanceof Blob)
101+
&& !(body instanceof ArrayBuffer)
102+
&& !ArrayBuffer.isView(body)
103+
&& !(body instanceof URLSearchParams)
104+
&& !(body instanceof ReadableStream)
105+
)
106+
)) {
80107
requestOptions.body = JSON.stringify(body);
81108
}
109+
else {
110+
requestOptions.body = body as BodyInit;
111+
}
82112

83113
const response = await fetch(this.url(url), requestOptions);
84114

services/osm.ts

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import parseOsmChangeXml from '@osmcha/osmchange-parser';
22
import type { FeatureCollection, Point } from 'geojson';
33

4-
import { BaseHttpClient, BaseHttpClientError } from '~/services/http';
4+
import {
5+
BaseHttpClient,
6+
BaseHttpClientError,
7+
type FetchConfig,
8+
type HttpBody,
9+
} from '~/services/http';
510
import * as xml from '~/util/xml';
611

712
import type { ICancelableClient } from '~/services/loading';
@@ -205,7 +210,9 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
205210
this.#webUrl = webUrl;
206211
this.#tdeiClient = tdeiClient;
207212
this.#setAuthHeader();
208-
this._requestHeaders['Accept'] = 'text/plain';
213+
214+
// OSM API can return XML or JSON based on the header or file extension:
215+
this._requestHeaders['Accept'] = '*/*';
209216
this._requestHeaders['Content-Type'] = 'text/plain';
210217
}
211218

@@ -232,8 +239,8 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
232239
display_name: this.auth.displayName
233240
};
234241

235-
await this._put(`user/${this.auth.subject}`, JSON.stringify(body), {
236-
headers: { ...this._requestHeaders, 'Content-Type': 'application/json' }
242+
await this._put(`user/${this.auth.subject}`, body, {
243+
headers: { 'Content-Type': 'application/json' }
237244
});
238245
}
239246

@@ -284,7 +291,6 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
284291
): Promise<OsmElement> {
285292
const response = await this._get(`${type}/${id}/${version}`, {
286293
headers: {
287-
...this._requestHeaders,
288294
'Accept': 'application/json',
289295
'X-Workspace': workspaceId,
290296
},
@@ -299,7 +305,6 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
299305
async getNodes(workspaceId: WorkspaceId, nodeIds: (number | string)[]): Promise<OsmNode[]> {
300306
const response = await this._get(`nodes?nodes=${nodeIds.join(',')}`, {
301307
headers: {
302-
...this._requestHeaders,
303308
'Accept': 'application/json',
304309
'X-Workspace': workspaceId,
305310
},
@@ -317,7 +322,6 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
317322
async getWays(workspaceId: WorkspaceId, wayIds: (number | string)[]): Promise<OsmWay[]> {
318323
const response = await this._get(`ways?ways=${wayIds.join(',')}`, {
319324
headers: {
320-
...this._requestHeaders,
321325
'Accept': 'application/json',
322326
'X-Workspace': workspaceId,
323327
},
@@ -335,7 +339,6 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
335339
async getWaysForNode(workspaceId: WorkspaceId, nodeId: number): Promise<OsmElement[]> {
336340
const response = await this._get(`node/${nodeId}/ways`, {
337341
headers: {
338-
...this._requestHeaders,
339342
'Accept': 'application/json',
340343
'X-Workspace': workspaceId,
341344
},
@@ -346,7 +349,7 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
346349

347350
async listChangesets(workspaceId: WorkspaceId): Promise<OsmChangeset[]> {
348351
const response = await this._get(`changesets.json`, {
349-
headers: { ...this._requestHeaders, 'X-Workspace': workspaceId },
352+
headers: { 'X-Workspace': workspaceId },
350353
});
351354

352355
const changesets = (await response.json())?.changesets ?? [];
@@ -372,7 +375,6 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
372375

373376
const response = await this._get(url, {
374377
headers: {
375-
...this._requestHeaders,
376378
'Accept': 'application/json',
377379
'X-Workspace': workspaceId,
378380
},
@@ -398,7 +400,6 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
398400
{
399401
const response = await this._get(`changeset/${changesetId}/download`, {
400402
headers: {
401-
...this._requestHeaders,
402403
'Accept': 'application/xml',
403404
'X-Workspace': workspaceId,
404405
},
@@ -424,7 +425,7 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
424425

425426
const body = xml.serialize(doc);
426427
const response = await this._put('changeset/create', body, {
427-
headers: { ...this._requestHeaders, 'X-Workspace': workspaceId },
428+
headers: { 'X-Workspace': workspaceId },
428429
});
429430

430431
return Number(await response.text());
@@ -438,7 +439,6 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
438439
await this._post(`changeset/${changesetId}/upload`, changesetXml, {
439440
headers: {
440441
'Content-Type': 'application/xml',
441-
'Authorization': this._requestHeaders['Authorization'],
442442
'X-Workspace': workspaceId,
443443
},
444444
});
@@ -465,10 +465,7 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
465465
body.append('text', message);
466466

467467
await this._post(`changeset/${changesetId}/comment`, body, {
468-
headers: {
469-
'Authorization': this._requestHeaders['Authorization'],
470-
'X-Workspace': workspaceId,
471-
},
468+
headers: { 'X-Workspace': workspaceId },
472469
});
473470
}
474471

@@ -481,7 +478,6 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
481478

482479
const response = await this._get(`notes/search.json?${params}`, {
483480
headers: {
484-
...this._requestHeaders,
485481
'Accept': 'application/json',
486482
'X-Workspace': workspaceId,
487483
},
@@ -494,7 +490,6 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
494490
const bboxParam = await this.getExportBbox(workspaceId);
495491
const response = await this._get(`map.json?bbox=${bboxParam}`, {
496492
headers: {
497-
...this._requestHeaders,
498493
'Accept': 'application/json',
499494
'X-Workspace': workspaceId
500495
}
@@ -507,7 +502,6 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
507502
const bboxParam = await this.getExportBbox(workspaceId);
508503
const response = await this._get(`map?bbox=${bboxParam}`, {
509504
headers: {
510-
...this._requestHeaders,
511505
'Accept': 'application/xml',
512506
'X-Workspace': workspaceId
513507
}
@@ -522,17 +516,23 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
522516
}
523517
}
524518

525-
override async _send(url: string, method: string, body?: any, config?: object): Promise<Response> {
519+
override async _send(
520+
url: string,
521+
method: string,
522+
body?: HttpBody,
523+
config?: FetchConfig,
524+
): Promise<Response> {
526525
try {
527526
await this.#tdeiClient.tryRefreshAuth();
528527
this.#setAuthHeader();
529528

530-
const requestOptions = {
531-
credentials: 'include'
532-
}
529+
const requestOptions: FetchConfig = {
530+
credentials: 'include',
531+
};
533532

534533
return await super._send(url, method, body, { ...requestOptions, ...config });
535-
} catch (e: any) {
534+
}
535+
catch (e: unknown) {
536536
if (e instanceof BaseHttpClientError) {
537537
throw new OsmApiClientError(e.response);
538538
}

services/tdei.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { BlobReader, BlobWriter, ZipReader } from '@zip.js/zip.js';
22
import type { FileEntry } from '@zip.js/zip.js';
33

4-
import { BaseHttpClient, BaseHttpClientError } from '~/services/http';
4+
import {
5+
BaseHttpClient,
6+
BaseHttpClientError,
7+
type FetchConfig,
8+
type HttpBody,
9+
} from '~/services/http';
510
import type { ICancelableClient } from '~/services/loading';
611
import type {
712
TdeiFeedback,
@@ -238,13 +243,17 @@ export class TdeiClient extends BaseHttpClient implements ICancelableClient {
238243
}
239244

240245
async downloadOswDataset(tdeiRecordId: string, format: string = 'osw'): Promise<Blob> {
241-
const response = await this._sendTest(`osw/${tdeiRecordId}?format=${format}`, 'GET');
246+
const response = await this._get(`osw/${tdeiRecordId}?format=${format}`, {
247+
headers: { Accept: '*/*' },
248+
});
242249

243250
return (await response.blob());
244251
}
245252

246253
async downloadPathwaysDataset(tdeiDatasetId: string): Promise<Blob> {
247-
const response = await this._sendTest(`gtfs-pathways/${tdeiDatasetId}`, 'GET');
254+
const response = await this._get(`gtfs-pathways/${tdeiDatasetId}`, {
255+
headers: { Accept: '*/*' },
256+
});
248257

249258
return (await response.blob());
250259
}
@@ -332,7 +341,7 @@ export class TdeiClient extends BaseHttpClient implements ICancelableClient {
332341
const filename = sourceFormat === 'osw' ? 'osw.zip' : 'osm.xml';
333342
body.append('file', new File([dataset], filename));
334343

335-
const jobResponse = await this._sendTest('osw/convert', 'POST', body);
344+
const jobResponse = await this._post('osw/convert', body);
336345
const jobId = (await jobResponse.text());
337346

338347
while (true) {
@@ -357,7 +366,9 @@ export class TdeiClient extends BaseHttpClient implements ICancelableClient {
357366
}
358367
}
359368

360-
const fileResponse = await this._sendTest(`job/download/${jobId}`, 'GET');
369+
const fileResponse = await this._get(`job/download/${jobId}`, {
370+
headers: { 'Accept': '*/*' },
371+
});
361372

362373
return await fileResponse.blob();
363374
}
@@ -429,14 +440,20 @@ export class TdeiClient extends BaseHttpClient implements ICancelableClient {
429440
}
430441
}
431442

432-
override async _send(url: string, method: string, body?: any, config?: object): Promise<Response> {
443+
override async _send(
444+
url: string,
445+
method: string,
446+
body?: HttpBody,
447+
config?: FetchConfig,
448+
): Promise<Response> {
433449
try {
434450
if (this.#auth.needsRefresh) {
435451
await this.refreshToken();
436452
}
437453

438454
return await super._send(url, method, body, config);
439-
} catch (e: any) {
455+
}
456+
catch (e: unknown) {
440457
if (e instanceof BaseHttpClientError) {
441458
throw new TdeiClientError(e.response);
442459
}
@@ -520,13 +537,19 @@ export class TdeiUserClient extends BaseHttpClient implements ICancelableClient
520537
}
521538
}
522539

523-
override async _send(url: string, method: string, body?: any, config?: object): Promise<Response> {
540+
override async _send(
541+
url: string,
542+
method: string,
543+
body?: HttpBody,
544+
config?: FetchConfig,
545+
): Promise<Response> {
524546
try {
525547
await this.#tdeiClient.tryRefreshAuth();
526548
this.#setAuthHeader();
527549

528550
return await super._send(url, method, body, config);
529-
} catch (e: any) {
551+
}
552+
catch (e: unknown) {
530553
if (e instanceof BaseHttpClientError) {
531554
throw new TdeiUserClientError(e.response);
532555
}

services/workspaces.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { BaseHttpClient, BaseHttpClientError } from '~/services/http';
1+
import {
2+
BaseHttpClient,
3+
BaseHttpClientError,
4+
type FetchConfig,
5+
type HttpBody,
6+
} from '~/services/http';
27
import { buildPathwaysCsvArchive } from '~/services/pathways';
38
import { compareStringAsc } from '~/util/compare';
49

@@ -238,15 +243,16 @@ export class WorkspacesClient extends BaseHttpClient implements ICancelableClien
238243
override async _send(
239244
url: string,
240245
method: string,
241-
body?: any,
242-
config?: object
246+
body?: HttpBody,
247+
config?: FetchConfig,
243248
): Promise<Response> {
244249
try {
245250
await this.#tdeiClient.tryRefreshAuth();
246251
this.#setAuthHeader();
247252

248253
return await super._send(url, method, body, config);
249-
} catch (e) {
254+
}
255+
catch (e: unknown) {
250256
if (e instanceof BaseHttpClientError) {
251257
throw new WorkspacesClientError(e.response);
252258
}

0 commit comments

Comments
 (0)