forked from camptocamp/ogc-client
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhttp-utils.ts
More file actions
161 lines (151 loc) · 5.08 KB
/
http-utils.ts
File metadata and controls
161 lines (151 loc) · 5.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import { parseXmlString } from './xml-utils.js';
import { EndpointError } from './errors.js';
import { decodeString } from './encoding.js';
import { FetchOptions } from './models.js';
const fetchPromises: Map<string, Promise<Response>> = new Map();
let fetchOptions: FetchOptions = {};
let fetchOptionsUpdateCallback: (options: FetchOptions) => void = null;
/**
* Set advanced options to be used by all fetch() calls
* @param options
*/
export function setFetchOptions(options: FetchOptions) {
fetchOptions = options;
if (fetchOptionsUpdateCallback) fetchOptionsUpdateCallback(options);
}
/**
* Returns current fetch() options
*/
export function getFetchOptions() {
return fetchOptions;
}
/**
* Resets advanced fetch() options to their defaults
*/
export function resetFetchOptions() {
fetchOptions = {};
if (fetchOptionsUpdateCallback) fetchOptionsUpdateCallback({});
}
export function setFetchOptionsUpdateCallback(
callback: (options: FetchOptions) => void
) {
fetchOptionsUpdateCallback = callback;
}
/**
* Returns a promise equivalent to `fetch(url)` but guarded against
* identical concurrent requests
* Note: this should only be used for GET requests!
*/
export function sharedFetch(
url: string,
method: 'GET' | 'HEAD' = 'GET',
asJson?: boolean,
customAcceptHeader?: string
) {
let fetchKey = `${method}#${url}`;
if (asJson || customAcceptHeader) {
fetchKey = `${method}#asJson#${url}`;
}
if (fetchPromises.has(fetchKey)) {
return fetchPromises.get(fetchKey);
}
const options: RequestInit = { ...getFetchOptions() };
options.method = method;
if (customAcceptHeader) {
options.headers = 'headers' in options ? options.headers : {};
options.headers['Accept'] = customAcceptHeader;
} else if (asJson) {
options.headers = 'headers' in options ? options.headers : {};
options.headers['Accept'] = 'application/json,application/schema+json';
}
// to avoid unhandled promise rejections this promise will never reject,
// but only return errors as a normal value
const promise = fetch(url, options)
.catch((e) => e)
.then((resp) => {
fetchPromises.delete(fetchKey);
return resp;
});
fetchPromises.set(fetchKey, promise);
// if an error is received then the promise will reject with it
return promise.then((resp) => {
if (resp instanceof Error) throw resp;
return resp.clone(); // clone response so that it can be used many times
});
}
/**
* Runs a GET HTTP request to the provided URL and resolves to the
* XmlDocument
*/
export function queryXmlDocument(url: string) {
return sharedFetch(url)
.catch(() =>
// attempt a HEAD to see if the failure comes from CORS or the service is generally unreachable
fetch(url, { ...getFetchOptions(), method: 'HEAD', mode: 'no-cors' })
.catch((error) => {
throw new EndpointError(
`Fetching the document failed either due to network errors or unreachable host, error is: ${error.message}`,
0,
false
);
})
.then(() => {
throw new EndpointError(
`The document could not be fetched due to CORS limitations`,
0,
true
);
})
)
.then(async (resp: Response) => {
if (!resp.ok) {
const text = await resp.text();
throw new EndpointError(
`Received an error with code ${resp.status}: ${text}`,
resp.status,
false
);
}
const buffer = await resp.arrayBuffer();
const contentTypeHeader = resp.headers.get('Content-Type');
return decodeString(buffer, contentTypeHeader);
})
.then((xml) => parseXmlString(xml));
}
/**
* Add or replace query params in the url; note that params are considered case-insensitive,
* meaning that existing params in different cases will be removed as well.
* Also, if the url ends with an encoded URL (typically in the case of urls run through a CORS
* proxy, which is an aberration and should be forbidden btw), then the encoded URL
* will be modified instead.
*/
export function setQueryParams(
url: string,
params: Record<string, string | boolean>
): string {
const encodedUrlMatch = url.match(/(https?%3A%2F%2F[^/]+)$/);
if (encodedUrlMatch) {
const encodedUrl = encodedUrlMatch[1];
const modifiedUrl = setQueryParams(decodeURIComponent(encodedUrl), params);
return url.replace(encodedUrl, encodeURIComponent(modifiedUrl));
}
const urlObj = new URL(url);
const keys = Object.keys(params);
const keysLower = keys.map((key) => key.toLowerCase());
const toDelete = [];
for (const param of urlObj.searchParams.keys()) {
if (keysLower.indexOf(param.toLowerCase()) > -1) {
toDelete.push(param);
}
}
toDelete.map((param) => urlObj.searchParams.delete(param));
keys.forEach((key) =>
urlObj.searchParams.set(
key,
params[key] === true ? '' : (params[key] as string)
)
);
// this makes sure that the request will work on GeoServer (some versions fail if there is a "+" in the encoded query params)
urlObj.search = urlObj.search.replace(/\+/g, '%20');
return urlObj.toString();
}