Setting the file. One moment.
Wp HTTP · Rp Source Wordpress · wix/skills · Skills Docs
ContentsBack to the top of the page
Number 43.13
Position 13 of 46
Type JavaScript
Size 8 KB
Lines 255 lib/ wp-http.js
JavaScript · 255 lines · 8 KB
15
// vendored copy needs no install step.
16
17 const DEFAULT_TIMEOUT_MS = 60000 ;
18 const DEFAULT_RATE_LIMIT_RPM = 120 ;
19 const DEFAULT_MAX_RETRIES = 3 ;
20 const MAX_BACKOFF_MS = 30000 ;
21
22 // Shared throttle state. Every request routes through fetchJson, so a single
23 // module-level throttle enforces the rate limit across the whole run.
24 const rateState = { minIntervalMs: 0 , lastRequestAt: 0 , maxRetries: DEFAULT_MAX_RETRIES };
25
26 function sleep ( ms ) {
27 return new Promise (( resolve ) => setTimeout (resolve, ms));
28 }
29
30 // Configure the shared throttle. Call once before issuing requests.
31 function configureRateLimit ({ rateLimitRpm = DEFAULT_RATE_LIMIT_RPM , maxRetries = DEFAULT_MAX_RETRIES } = {}) {
32 const rpm = Number. isFinite (rateLimitRpm) && rateLimitRpm >= 1 ? rateLimitRpm : DEFAULT_RATE_LIMIT_RPM ;
33 rateState.minIntervalMs = Math. ceil ( 60000 / rpm);
34 rateState.maxRetries = Number. isFinite (maxRetries) && maxRetries >= 0 ? maxRetries : DEFAULT_MAX_RETRIES ;
35 }
36
37 async function applyThrottle ( progress ) {
38 if (rateState.minIntervalMs <= 0 ) {
39 return ;
40 }
41 const elapsed = Date. now () - rateState.lastRequestAt;
42 const wait = rateState.minIntervalMs - elapsed;
43 if (wait > 0 ) {
44 if (progress) {
45 await progress. withHeartbeat ({ phase: 'discovery' , step: 'rate-limit' , message: 'Still waiting for WordPress rate limit' }, () => sleep (wait));
46 } else {
47 await sleep (wait);
48 }
49 }
50 rateState.lastRequestAt = Date. now ();
51 }
52
53 function retryAfterMs ( responseHeaders ) {
54 const raw = responseHeaders?.[ 'retry-after' ];
55 if ( ! raw) {
56 return null ;
57 }
58 const seconds = Number. parseInt (raw, 10 );
59 return Number. isFinite (seconds) ? seconds * 1000 : null ;
60 }
61
62 function buildHeaders ( args ) {
63 const headers = new Headers ();
64 headers. set ( 'accept' , 'application/json' );
65
66 for ( const rawHeader of args.authHeaders || []) {
67 const splitIndex = rawHeader. indexOf ( ':' );
68 if (splitIndex === - 1 ) {
69 throw new Error ( `Invalid --auth-header value: ${ rawHeader }` );
70 }
71 const name = rawHeader. slice ( 0 , splitIndex). trim ();
72 const value = rawHeader. slice (splitIndex + 1 ). trim ();
73 headers. set (name, value);
74 }
75
76 if (args.username && args.applicationPassword) {
77 const credentials = Buffer. from ( `${ args . username }:${ args . applicationPassword }` ). toString ( 'base64' );
78 headers. set ( 'authorization' , `Basic ${ credentials }` );
79 } else if (args.apiKey) {
80 const headerName = args.apiKeyHeader || 'Authorization' ;
81 const headerValue = headerName. toLowerCase () === 'authorization' && ! / ^ bearer \s + / i . test (args.apiKey)
82 ? `Bearer ${ args . apiKey }`
83 : args.apiKey;
84 headers. set (headerName, headerValue);
85 }
86
87 return headers;
88 }
89
90 function normalizeBaseUrl ( input ) {
91 return input. replace ( / \/ +$ / , '' );
92 }
93
94 function buildApiUrl ( baseUrl , routePath , query = {}) {
95 const url = new URL ( `${ normalizeBaseUrl ( baseUrl ) }/wp-json${ routePath }` );
96 for ( const [ key , value ] of Object. entries (query)) {
97 if (value === undefined || value === null || value === '' ) {
98 continue ;
99 }
100 url.searchParams. set (key, String (value));
101 }
102 return url;
103 }
104
105 async function fetchJson ( baseUrl , routePath , { headers , method = 'GET' , query , body , timeoutMs = DEFAULT_TIMEOUT_MS , progress = null , progressContext = {} }) {
106 const url = buildApiUrl (baseUrl, routePath, query);
107 // `body` is a small, generic escape hatch for profile-declared entities whose read path
108 // is not a plain GET collection (see plugin-knowledge.js buildRequestOverrides) — it is
109 // never set on the default GET-with-query sampling path.
110 const requestInit = {
111 method,
112 headers,
113 ... (body !== undefined ? { body } : {}),
114 };
115
116 for ( let attempt = 0 ; ; attempt += 1 ) {
117 await applyThrottle (progress);
118 const controller = new AbortController ();
119 const timeout = setTimeout (() => controller. abort (), timeoutMs);
120
121 try {
122 const response = progress
123 ? await progress. withHeartbeat ({
124 phase: 'discovery' ,
125 step: 'wordpress-request' ,
126 ... progressContext,
127 message: `Still waiting on WordPress ${ method } ${ routePath || '/'}` ,
128 }, () => fetch (url, {
129 ... requestInit,
130 signal: controller.signal,
131 }))
132 : await fetch (url, {
133 ... requestInit,
134 signal: controller.signal,
135 });
136 const text = await response. text ();
137 const responseHeaders = Object. fromEntries (response.headers. entries ());
138
139 // Back off and retry on throttling / transient unavailability,
140 // honoring Retry-After when the server provides it.
141 if ((response.status === 429 || response.status === 503 ) && attempt < rateState.maxRetries) {
142 const backoff = retryAfterMs (responseHeaders) ?? Math. min ( MAX_BACKOFF_MS , 1000 * 2 ** attempt);
143 if (progress) {
144 progress. warn ( `WordPress ${ response . status } response; backing off before retry` , {
145 phase: 'discovery' ,
146 step: 'wordpress-retry' ,
147 ... progressContext,
148 count: attempt + 1 ,
149 total: rateState.maxRetries,
150 unit: 'retries' ,
151 });
152 await progress. withHeartbeat ({
153 phase: 'discovery' ,
154 step: 'wordpress-retry' ,
155 ... progressContext,
156 message: `Still backing off after WordPress ${ response . status }` ,
157 }, () => sleep (backoff));
158 } else {
159 await sleep (backoff);
160 }
161 continue ;
162 }
163
164 let json;
165 if (text) {
166 try {
167 json = JSON . parse (text);
168 } catch {
169 json = undefined ;
170 }
171 }
172
173 return {
174 ok: response.ok,
175 status: response.status,
176 statusText: response.statusText,
177 url: url. toString (),
178 headers: responseHeaders,
179 json,
180 text,
181 retries: attempt,
182 };
183 } catch (error) {
184 return {
185 ok: false ,
186 status: 0 ,
187 statusText: error.name === 'AbortError' ? 'Request Timeout' : error.message,
188 url: url. toString (),
189 headers: {},
190 json: undefined ,
191 text: '' ,
192 error,
193 retries: attempt,
194 };
195 } finally {
196 clearTimeout (timeout);
197 }
198 }
199 }
200
201 // Total record count for a collection. WordPress returns it in X-WP-Total;
202 // X-WP-TotalPages carries the page count. Header names are lowercased by fetch's
203 // Headers iterator, but accept the canonical casing too for resilience.
204 function parseTotalHeader ( responseHeaders , name = 'x-wp-total' ) {
205 if ( ! responseHeaders) {
206 return null ;
207 }
208 // fetch's Headers iterator lowercases names, but match case-insensitively so a
209 // plain object built with WordPress's canonical casing (X-WP-Total) also works.
210 const target = name. toLowerCase ();
211 let raw;
212 for ( const [ key , value ] of Object. entries (responseHeaders)) {
213 if (key. toLowerCase () === target) {
214 raw = value;
215 break ;
216 }
217 }
218 if (raw === undefined || raw === null || raw === '' ) {
219 return null ;
220 }
221 const total = Number. parseInt (raw, 10 );
222 return Number. isFinite (total) ? total : null ;
223 }
224
225 function parseTotalPagesHeader ( responseHeaders ) {
226 return parseTotalHeader (responseHeaders, 'x-wp-totalpages' );
227 }
228
229 function shouldContinueCollectionPaging ({ responseHeaders , page , perPage , itemCount }) {
230 const totalPages = parseTotalPagesHeader (responseHeaders);
231 if (totalPages !== null ) {
232 return Number (page) < totalPages;
233 }
234 if (Number. isFinite (perPage) && perPage > 0 ) {
235 return Number (itemCount) >= perPage;
236 }
237 return false ;
238 }
239
240 module . exports = {
241 DEFAULT_TIMEOUT_MS,
242 DEFAULT_RATE_LIMIT_RPM,
243 DEFAULT_MAX_RETRIES,
244 MAX_BACKOFF_MS,
245 sleep,
246 configureRateLimit,
247 retryAfterMs,
248 buildHeaders,
249 normalizeBaseUrl,
250 buildApiUrl,
251 fetchJson,
252 parseTotalHeader,
253 parseTotalPagesHeader,
254 shouldContinueCollectionPaging,
255 };