Skip to content

Conversation

@gocanto
Copy link
Contributor

@gocanto gocanto commented Sep 1, 2025

Summary by CodeRabbit

  • Chores

    • Bumped development tooling dependencies for improved compatibility and stability.
  • Refactor

    • Switched to a server-backed signing flow for API requests and standardized timestamp handling to improve security and consistency.
    • Simplified client API usage to reduce client-side signing complexity.
  • Bug Fixes

    • Improved error handling with clearer failures for network/server errors and preserved caching behavior for unchanged resources.

@coderabbitai
Copy link

coderabbitai bot commented Sep 1, 2025

Walkthrough

Request signing moved from local HMAC to a remote signature service. ApiClient now requests a SignatureResponse from /generate-signature, appends X-API-Nonce, X-API-Timestamp, X-API-Intended-Origin and X-API-Signature headers, and throws HttpError on non-ok responses. Store calls removed nonce parameters. Added SignatureResponse interface and two devDependency bumps.

Changes

Cohort / File(s) Summary
Dev tooling bump
package.json
Bumped devDependencies: @vitejs/plugin-vue ^5.2.1 → ^6.0.1, eslint-config-prettier ^10.1.2 → ^10.1.8.
API client remote signature flow
src/stores/api/client.ts
Removed local sha256/hmac methods and timestamp property. Added private getSignature(nonce, origin) returning SignatureResponse and appendSignature(...) to set X-API-Nonce, X-API-Timestamp, X-API-Intended-Origin, X-API-Signature. get/post no longer accept nonce param, call appendSignature, and throw HttpError on non-ok responses.
API response types
src/stores/api/response/signature-response.ts
Added export interface SignatureResponse { signature: string; tries: number; cadence: { received_at: string; created_at: string; expires_at: string } }.
Store callsites (nonce removal)
src/stores/api/store.ts
Removed per-call createNonce() usage and removed nonce arguments from all this.client.get(...) and this.client.post(...) invocations.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor U as Caller
    participant C as ApiClient
    participant S as Signature Service (/generate-signature)
    participant A as API Server

    U->>C: get(url) / post(url, data)
    Note over C: create nonce, timestamp, origin
    C->>S: POST /generate-signature { nonce, origin }
    alt signature OK
        S-->>C: SignatureResponse (signature, tries, cadence)
        C->>A: GET/POST url\nHeaders: X-API-Nonce, X-API-Timestamp,\nX-API-Intended-Origin, X-API-Signature
        alt 200 OK
            A-->>C: 200 + body
            C-->>U: parsed result
        else 304 Not Modified
            A-->>C: 304
            C-->>U: serve cached
        else Error
            A-->>C: non-ok
            C-->>U: throw HttpError
        end
    else signature error
        S-->>C: non-ok
        C-->>U: throw HttpError
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Poem

I twitch my whiskers, fetch a key,
I skip the HMAC, hop to the sea.
A signed reply, a header bright,
I stamp my nonce, then take to flight.
Carrots and cache — requests just right. 🥕🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/token-endpoint

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gocanto gocanto marked this pull request as ready for review September 5, 2025 09:21
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/stores/api/client.ts (2)

119-136: Fix body double-read error in POST method.

The error handling reads the response body twice, causing the "Body is unusable" error seen in pipeline failures.

 	public async post<T>(url: string, nonce: string, data: object): Promise<T> {
 		const headers = this.createHeaders();
 		const fullUrl = new URL(url, this.basedURL);
 
 		await this.appendSignature(nonce, headers, fullUrl.href);
 
 		const response = await fetch(fullUrl.href, {
 			method: 'POST',
 			headers: headers,
 			body: JSON.stringify(data),
 		});
 
 		if (!response.ok) {
-			throw new HttpError(response, await response.text());
+			const errorBody = await response.text();
+			throw new HttpError(response, errorBody);
 		}
 
 		return await response.json();
 	}

138-173: Fix body double-read error in GET method.

Same issue as in the POST method - the error handling reads the response body twice.

 	public async get<T>(url: string, nonce: string): Promise<T> {
 		const headers = this.createHeaders();
 		const cached = this.getFromCache<T>(url);
 		const fullUrl = new URL(url, this.basedURL);
 
 		if (cached) {
 			headers.append('If-None-Match', cached.etag);
 		}
 
 		await this.appendSignature(nonce, headers, fullUrl.href);
 
 		const response = await fetch(fullUrl.href, {
 			method: 'GET',
 			headers: headers,
 		});
 
 		if (response.status === 304) {
 			console.log(`%c[CACHE] 304 Not Modified for "${url}". Serving from cache.`, 'color: #007acc;');
 
 			// We can safely assert cached is not null here.
 			return cached!.data;
 		}
 
 		if (!response.ok) {
-			throw new HttpError(response, await response.text());
+			const errorBody = await response.text();
+			throw new HttpError(response, errorBody);
 		}
 
 		const eTag = response.headers.get('ETag');
 		const payload = await response.json();
 
 		if (eTag) {
 			this.setToCache(url, eTag, payload);
 		}
 
 		return payload;
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3e372bb and 8d62a98.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • package.json (1 hunks)
  • src/stores/api/client.ts (4 hunks)
  • src/stores/api/response/signature-response.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/stores/api/client.ts (2)
src/stores/api/response/signature-response.ts (1)
  • SignatureResponse (1-9)
src/stores/api/http-error.ts (1)
  • HttpError (1-15)
🪛 GitHub Check: vitest
src/stores/api/client.ts

[failure] 105-105: tests/stores/api/client.test.ts > ApiClient > caches get requests and serves from cache
HttpError: API request failed with status 304:
❯ ApiClient.getSignature src/stores/api/client.ts:105:10
❯ ApiClient.appendSignature src/stores/api/client.ts:112:19
❯ ApiClient.get src/stores/api/client.ts:147:3
❯ tests/stores/api/client.test.ts:68:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { body: '', status: 304 }

🪛 GitHub Actions: Unit Tests
src/stores/api/client.ts

[error] 135-135: TypeError: Body is unusable: Body has already been read


[error] 105-105: HttpError: API request failed with status 304


[error] 166-166: TypeError: Body is unusable: Body has already been read

🔇 Additional comments (4)
package.json (1)

35-35: DevDependency updates look good.

The version bumps for @vitejs/plugin-vue (5.2.1 → 6.0.1) and eslint-config-prettier (10.1.2 → 10.1.8) are reasonable updates that shouldn't affect production code.

Also applies to: 39-39

src/stores/api/response/signature-response.ts (1)

1-9: Interface definition looks good.

The SignatureResponse interface properly defines the structure for the remote signature service response with appropriate types.

src/stores/api/client.ts (2)

80-80: Confirm timestamp format compatibility (src/stores/api/client.ts:80): No tests or documentation specify the expected format—verify with the signature service that it accepts a stringified Unix‐second timestamp and consider adding corresponding tests or docs.


87-109: Multiple critical issues in getSignature method.

  1. Body double-read error: The response body is read twice (lines 105 and 108), causing the "Body is unusable" error
  2. HTTP 304 handling: The method throws an error for 304 status, but 304 is used for caching in the GET method

Fix the body double-read issue:

 	private async getSignature(nonce: string, origin: string): Promise<SignatureResponse> {
 		const headers = this.createHeaders();
 		const fullUrl = new URL('generate-signature', this.basedURL);
 
 		headers.append('X-API-Intended-Origin', origin);
 
 		const response = await fetch(fullUrl.href, {
 			method: 'POST',
 			headers: headers,
 			body: JSON.stringify({
 				nonce: nonce,
 				public_key: this.apiKey,
 				username: this.apiUsername,
-				timestamp: this.timestamp,
+				timestamp: this.getCurrentTimestamp(),
 			}),
 		});
 
 		if (!response.ok) {
-			throw new HttpError(response, await response.text());
+			const errorBody = await response.text();
+			throw new HttpError(response, errorBody);
 		}
 
 		return await response.json();
 	}

Likely an incorrect or invalid review comment.

@gocanto gocanto merged commit 503f1cd into main Sep 8, 2025
1 of 3 checks passed
@gocanto gocanto deleted the feature/token-endpoint branch September 8, 2025 02:01
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/stores/api/client.ts (1)

143-161: Update tests to use new client.get/post signatures
In tests/stores/api/client.test.ts (lines 52–59, 67–90), replace

  • client.get('…', nonce)client.get('…')
  • client.post('…', nonce, data)client.post('…', data)
♻️ Duplicate comments (1)
src/stores/api/client.ts (1)

89-92: Make signature endpoint configurable and enforce HTTPS (prod).

Use env-configurable endpoint instead of hardcoded path; keep HTTPS guard.

-    const fullUrl = new URL('generate-signature', this.basedURL);
+    const endpoint = import.meta.env.VITE_SIGNATURE_ENDPOINT || 'generate-signature';
+    const fullUrl = new URL(endpoint, this.basedURL);
🧹 Nitpick comments (2)
src/stores/api/client.ts (2)

35-40: Optional: enforce HTTPS for base API in production.

Guard against misconfiguration similar to the signature endpoint.

   constructor(options: ApiClientOptions) {
     this.env = options.env;
     this.apiKey = options.apiKey;
     this.apiUsername = options.apiUsername;
-    this.basedURL = `${import.meta.env.VITE_API_URL}`;
+    this.basedURL = `${import.meta.env.VITE_API_URL}`;
+    if (this.isProd() && new URL(this.basedURL).protocol !== 'https:') {
+      throw new Error('API base URL must be HTTPS in production');
+    }
   }

33-33: Nit: typo in property name.

Consider renaming basedURL → baseURL for clarity (wide refactor).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d62a98 and a831f45.

📒 Files selected for processing (2)
  • src/stores/api/client.ts (4 hunks)
  • src/stores/api/store.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/stores/api/store.ts (2)
src/stores/api/client.ts (1)
  • ApiResponse (24-27)
src/stores/api/http-error.ts (1)
  • parseError (17-27)
src/stores/api/client.ts (3)
src/stores/api/response/signature-response.ts (1)
  • SignatureResponse (1-9)
src/stores/api/http-error.ts (1)
  • HttpError (1-15)
tests/stores/api/client.test.ts (2)
  • cacheKey (52-61)
  • it (30-82)
🪛 GitHub Check: vitest
src/stores/api/client.ts

[failure] 111-111: tests/stores/api/client.test.ts > ApiClient > caches get requests and serves from cache
HttpError: API request failed with status 304:
❯ ApiClient.getSignature src/stores/api/client.ts:111:10
❯ ApiClient.appendSignature src/stores/api/client.ts:123:21
❯ ApiClient.get src/stores/api/client.ts:173:3
❯ tests/stores/api/client.test.ts:68:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { body: '', status: 304 }

🪛 GitHub Actions: Unit Tests
src/stores/api/client.ts

[error] 160-160: npm test: ApiClient.post() attempted to read the response body as JSON after it had already been read (Body is unusable: Body has already been read).


[error] 111-111: npm test: HttpError thrown for non-OK response (304) by reading response.text().


[error] 192-192: npm test: ApiClient.get() attempted to read response.json() after the body had already been read.

🔇 Additional comments (4)
src/stores/api/store.ts (2)

46-46: LGTM: store calls updated to new client signature (no nonce).

All invocations correctly drop the nonce and match the refactored client API.

Also applies to: 55-55, 64-64, 73-73, 82-82, 91-91, 100-100, 109-109, 118-118, 127-127


118-118: Confirm backend contract for posts now using POST. Ensure the /posts endpoint accepts POST with filters payload and that idempotency/caching (including any CDNs or proxies) behaves as expected; no other client.get calls for this endpoint remain outside tests.

src/stores/api/client.ts (2)

72-74: Good: per-request timestamp.

Fresh timestamps avoid stale-request rejections.


97-97: Verify origin format used for signing.

Confirm the signature service expects the full URL (href) vs just the origin (scheme+host). If origin-only is required, use new URL(origin).origin.

Comment on lines 76 to 85
private createHeaders(): Headers {
const headers = new Headers();

headers.append('X-API-Key', this.apiKey);
headers.append('X-Request-ID', uuidv4());
headers.append('User-Agent', 'oullin/web-app');
headers.append('X-API-Timestamp', this.timestamp);
headers.append('X-API-Username', this.apiUsername);
headers.append('Content-Type', 'application/json');
headers.append('X-API-Timestamp', this.getCurrentTimestamp().toString());

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Remove forbidden User-Agent header; set Content-Type only for POST.

Browsers block setting User-Agent; also avoid sending Content-Type on GET to prevent unnecessary preflights.

Apply:

   private createHeaders(): Headers {
     const headers = new Headers();

     headers.append('X-API-Key', this.apiKey);
     headers.append('X-Request-ID', uuidv4());
-    headers.append('User-Agent', 'oullin/web-app');
     headers.append('X-API-Username', this.apiUsername);
-    headers.append('Content-Type', 'application/json');
     headers.append('X-API-Timestamp', this.getCurrentTimestamp().toString());

     return headers;
   }

And set Content-Type only in post():

   public async post<T>(url: string, data: object): Promise<T> {
     const nonce = this.createNonce();
     const headers = this.createHeaders();
+    headers.set('Content-Type', 'application/json');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private createHeaders(): Headers {
const headers = new Headers();
headers.append('X-API-Key', this.apiKey);
headers.append('X-Request-ID', uuidv4());
headers.append('User-Agent', 'oullin/web-app');
headers.append('X-API-Timestamp', this.timestamp);
headers.append('X-API-Username', this.apiUsername);
headers.append('Content-Type', 'application/json');
headers.append('X-API-Timestamp', this.getCurrentTimestamp().toString());
private createHeaders(): Headers {
const headers = new Headers();
headers.append('X-API-Key', this.apiKey);
headers.append('X-Request-ID', uuidv4());
// Removed User-Agent
headers.append('X-API-Username', this.apiUsername);
// Content-Type moved to POST requests only
headers.append('X-API-Timestamp', this.getCurrentTimestamp().toString());
return headers;
}
public async post<T>(url: string, data: object): Promise<T> {
const nonce = this.createNonce();
const headers = this.createHeaders();
headers.set('Content-Type', 'application/json');
// ...rest of the POST logic...
}
🤖 Prompt for AI Agents
In src/stores/api/client.ts around lines 76 to 85, remove the
headers.append('User-Agent', ...) and stop adding 'Content-Type' in
createHeaders(); instead leave createHeaders() to add only safe headers (API
key, request id, username, timestamp). Add logic in the post() method to set
'Content-Type: application/json' just before sending the body (e.g., clone
headers and append Content-Type for POST requests) so GET requests never include
Content-Type and the forbidden User-Agent is never set.

Comment on lines +110 to +112
if (!response.ok) {
throw new HttpError(response, await response.text());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don’t read response body on non-OK; fixes test failures and double-read errors.

Reading response.text() here consumes a shared mocked Response, causing “Body has already been read” later.

-    if (!response.ok) {
-      throw new HttpError(response, await response.text());
-    }
+    if (!response.ok) {
+      throw new HttpError(response, null);
+    }

Apply the same change in post() and get() error paths.

Also applies to: 156-158, 187-189

🧰 Tools
🪛 GitHub Check: vitest

[failure] 111-111: tests/stores/api/client.test.ts > ApiClient > caches get requests and serves from cache
HttpError: API request failed with status 304:
❯ ApiClient.getSignature src/stores/api/client.ts:111:10
❯ ApiClient.appendSignature src/stores/api/client.ts:123:21
❯ ApiClient.get src/stores/api/client.ts:173:3
❯ tests/stores/api/client.test.ts:68:18

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { body: '', status: 304 }

🪛 GitHub Actions: Unit Tests

[error] 111-111: npm test: HttpError thrown for non-OK response (304) by reading response.text().

🤖 Prompt for AI Agents
In src/stores/api/client.ts around lines 110-112 (and similarly at 156-158 and
187-189), the code reads await response.text() on non-OK responses which
consumes the response body and causes "Body has already been read" errors in
tests; remove the body read and throw the HttpError with only the Response
object (e.g., throw new HttpError(response)) in each error path so the response
body is not consumed here and callers can decide whether to read it.

Comment on lines +117 to 141
private async appendSignature(nonce: string, headers: Headers, origin: string) {
const retries = 3;
let lastError: Error | undefined;

for (let i = 0; i < retries; i++) {
try {
const sigResp = await this.getSignature(nonce, origin);

headers.append('X-API-Nonce', nonce);
headers.append('X-API-Intended-Origin', origin);
headers.append('X-API-Signature', sigResp.signature);

const canonical = [method.toUpperCase(), uri.pathname || '/', this.getSortedQuery(uri.href), this.apiUsername, this.apiKey, this.timestamp, nonce, content].join('\n');
return;
} catch (error) {
lastError = error as Error;

if (i < retries - 1) {
// Exponential backoff: waits 1 s, then 2 s.
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
}

return await this.hmacSha256Hex(this.apiKey, canonical);
throw lastError || new Error('Failed to get signature after multiple retries.');
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden appendSignature: dev bypass, dedupe headers, faster backoff.

  • Use headers.set to avoid duplicates.
  • Provide a dev/test bypass to avoid hitting the signature service (unblocks 304 cache test).
  • Shorter backoff base to avoid long stalls in dev.
-  private async appendSignature(nonce: string, headers: Headers, origin: string) {
-    const retries = 3;
+  private async appendSignature(nonce: string, headers: Headers, origin: string) {
+    // Dev/test: skip remote signature to avoid brittle tests and speed local runs
+    if (this.isDev() && !import.meta.env.VITE_SIGNATURE_ENDPOINT) {
+      headers.set('X-API-Nonce', nonce);
+      headers.set('X-API-Intended-Origin', origin);
+      headers.set('X-API-Signature', 'dev'); // harmless placeholder
+      return;
+    }
+    const retries = 3;
     let lastError: Error | undefined;

     for (let i = 0; i < retries; i++) {
       try {
         const sigResp = await this.getSignature(nonce, origin);

-        headers.append('X-API-Nonce', nonce);
-        headers.append('X-API-Intended-Origin', origin);
-        headers.append('X-API-Signature', sigResp.signature);
+        headers.set('X-API-Nonce', nonce);
+        headers.set('X-API-Intended-Origin', origin);
+        headers.set('X-API-Signature', sigResp.signature);

         return;
       } catch (error) {
         lastError = error as Error;

         if (i < retries - 1) {
-          // Exponential backoff: waits 1 s, then 2 s.
-          await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000));
+          // Exponential backoff: ~100ms, 200ms
+          await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
         }
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private async appendSignature(nonce: string, headers: Headers, origin: string) {
const retries = 3;
let lastError: Error | undefined;
for (let i = 0; i < retries; i++) {
try {
const sigResp = await this.getSignature(nonce, origin);
headers.append('X-API-Nonce', nonce);
headers.append('X-API-Intended-Origin', origin);
headers.append('X-API-Signature', sigResp.signature);
const canonical = [method.toUpperCase(), uri.pathname || '/', this.getSortedQuery(uri.href), this.apiUsername, this.apiKey, this.timestamp, nonce, content].join('\n');
return;
} catch (error) {
lastError = error as Error;
if (i < retries - 1) {
// Exponential backoff: waits 1 s, then 2 s.
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
}
return await this.hmacSha256Hex(this.apiKey, canonical);
throw lastError || new Error('Failed to get signature after multiple retries.');
}
private async appendSignature(nonce: string, headers: Headers, origin: string) {
// Dev/test: skip remote signature to avoid brittle tests and speed local runs
if (this.isDev() && !import.meta.env.VITE_SIGNATURE_ENDPOINT) {
headers.set('X-API-Nonce', nonce);
headers.set('X-API-Intended-Origin', origin);
headers.set('X-API-Signature', 'dev'); // harmless placeholder
return;
}
const retries = 3;
let lastError: Error | undefined;
for (let i = 0; i < retries; i++) {
try {
const sigResp = await this.getSignature(nonce, origin);
headers.set('X-API-Nonce', nonce);
headers.set('X-API-Intended-Origin', origin);
headers.set('X-API-Signature', sigResp.signature);
return;
} catch (error) {
lastError = error as Error;
if (i < retries - 1) {
// Exponential backoff: ~100ms, 200ms
await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i)));
}
}
}
throw lastError || new Error('Failed to get signature after multiple retries.');
}
🤖 Prompt for AI Agents
In src/stores/api/client.ts around lines 117 to 141, appendSignature currently
uses headers.append (causing duplicate headers), always calls getSignature
(breaking dev/test flows), and uses long 1s base backoff; change headers.append
to headers.set to dedupe header values, add a dev/test bypass (e.g. when
NODE_ENV === 'test' or a passed-in client option) that skips getSignature and
sets X-API-Nonce, X-API-Intended-Origin, and a deterministic X-API-Signature
value for tests, and shorten the exponential backoff base (use 100ms base so
retries wait ~100ms, ~200ms, etc.) while preserving the retry loop and throwing
the last error if all attempts fail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants