Skip to content

Commit a082e85

Browse files
authored
fix: app user agent for api-client reqs using tauri http plugin (#6045)
fix: app user agent
1 parent 7048a35 commit a082e85

9 files changed

Lines changed: 80 additions & 66 deletions

File tree

‎apps/app-frontend/src/App.vue‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,9 @@ const popupNotificationManager = new AppPopupNotificationManager()
146146
providePopupNotificationManager(popupNotificationManager)
147147
const { addPopupNotification } = popupNotificationManager
148148
149+
const appVersion = getVersion()
149150
const tauriApiClient = new TauriModrinthClient({
150-
userAgent: `modrinth/theseus/${getVersion()} (support@modrinth.com)`,
151+
userAgent: async () => `modrinth/theseus/${await appVersion} (support@modrinth.com)`,
151152
labrinthBaseUrl: config.labrinthBaseUrl,
152153
archonBaseUrl: config.archonBaseUrl,
153154
features: [

‎packages/api-client/README.md‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { AuthFeature, GenericModrinthClient, type Labrinth } from '@modrinth/api
2828

2929
const client = new GenericModrinthClient({
3030
userAgent: 'my-app/1.0.0',
31-
features: [new AuthFeature({ token: 'mrp_...' })],
31+
features: [new AuthFeature({ token: process.env.MODRINTH_TOKEN })],
3232
})
3333

3434
const project: Labrinth.Projects.v2.Project = await client.labrinth.projects_v2.get('sodium')
@@ -51,14 +51,13 @@ import { AuthFeature, CircuitBreakerFeature, NuxtCircuitBreakerStorage, NuxtModr
5151

5252
export const useModrinthClient = async () => {
5353
const config = useRuntimeConfig()
54-
const auth = await useAuth()
5554

5655
return new NuxtModrinthClient({
5756
userAgent: 'my-nuxt-app/1.0.0',
5857
rateLimitKey: import.meta.server ? config.rateLimitKey : undefined,
5958
features: [
6059
new AuthFeature({
61-
token: async () => auth.value.token,
60+
token: process.env.MODRINTH_TOKEN,
6261
}),
6362
new CircuitBreakerFeature({
6463
storage: new NuxtCircuitBreakerStorage(),
@@ -74,10 +73,9 @@ export const useModrinthClient = async () => {
7473
import { getVersion } from '@tauri-apps/api/app'
7574
import { AuthFeature, TauriModrinthClient } from '@modrinth/api-client'
7675

77-
const version = await getVersion()
7876
const client = new TauriModrinthClient({
79-
userAgent: `modrinth/theseus/${version} (support@modrinth.com)`,
80-
features: [new AuthFeature({ token: 'mrp_...' })],
77+
userAgent: async () => `modrinth/theseus/${await getVersion()} (support@modrinth.com)`,
78+
features: [new AuthFeature({ token: process.env.MODRINTH_TOKEN })],
8179
})
8280

8381
const project = await client.labrinth.projects_v2.get('sodium')
@@ -138,7 +136,7 @@ Features wrap requests before they reach the platform implementation:
138136
import { AuthFeature, CircuitBreakerFeature, RetryFeature } from '@modrinth/api-client'
139137

140138
const client = new GenericModrinthClient({
141-
features: [new AuthFeature({ token: async () => getToken() }), new RetryFeature({ maxAttempts: 3, backoffStrategy: 'exponential' }), new CircuitBreakerFeature({ maxFailures: 3, resetTimeout: 30_000 })],
139+
features: [new AuthFeature({ token: async () => process.env.MODRINTH_TOKEN }), new RetryFeature({ maxAttempts: 3, backoffStrategy: 'exponential' }), new CircuitBreakerFeature({ maxFailures: 3, resetTimeout: 30_000 })],
142140
})
143141
```
144142

‎packages/api-client/src/core/abstract-client.ts‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,15 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
125125

126126
const url = this.buildUrl(path, baseUrl, options.version)
127127

128+
const defaultHeaders = await this.buildDefaultHeaders()
129+
128130
// Merge options with defaults
129131
const mergedOptions: RequestOptions = {
130132
method: 'GET',
131133
timeout: this.config.timeout,
132134
...options,
133135
headers: {
134-
...this.buildDefaultHeaders(),
136+
...defaultHeaders,
135137
...options.headers,
136138
},
137139
}
@@ -306,19 +308,25 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
306308
* Subclasses can override this to add platform-specific headers
307309
* (e.g., Nuxt rate limit key)
308310
*/
309-
protected buildDefaultHeaders(): Record<string, string> {
311+
protected async buildDefaultHeaders(): Promise<Record<string, string>> {
310312
const headers: Record<string, string> = {
311313
'Content-Type': 'application/json',
312314
...this.config.headers,
313315
}
314316

315-
if (this.config.userAgent) {
316-
headers['User-Agent'] = this.config.userAgent
317+
const userAgent = await this.resolveUserAgent()
318+
if (userAgent) {
319+
headers['User-Agent'] = userAgent
317320
}
318321

319322
return headers
320323
}
321324

325+
private async resolveUserAgent(): Promise<string | undefined> {
326+
const userAgent = this.config.userAgent
327+
return typeof userAgent === 'function' ? await userAgent() : userAgent
328+
}
329+
322330
protected attachArchonSentryCaptureHeader(options: RequestOptions): void {
323331
if (options.api !== 'archon' || !options.headers || !this.shouldCaptureArchonRequests()) {
324332
return
@@ -404,7 +412,7 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
404412
* @example
405413
* ```typescript
406414
* const client = new GenericModrinthClient()
407-
* client.addFeature(new AuthFeature({ token: 'mrp_...' }))
415+
* client.addFeature(new AuthFeature({ token: async () => getOAuthToken() }))
408416
* client.addFeature(new RetryFeature({ maxAttempts: 3 }))
409417
* ```
410418
*/

‎packages/api-client/src/features/auth.ts‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,8 @@ export interface AuthConfig extends FeatureConfig {
3333
*
3434
* @example
3535
* ```typescript
36-
* // Static token
3736
* const auth = new AuthFeature({
38-
* token: 'mrp_...'
39-
* })
40-
*
41-
* // Dynamic token (e.g., from auth state)
42-
* const auth = new AuthFeature({
43-
* token: async () => await getAuthToken()
37+
* token: async () => process.env.MODRINTH_TOKEN
4438
* })
4539
* ```
4640
*/

‎packages/api-client/src/platform/generic.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { XHRUploadClient } from './xhr-upload-client'
1616
* const client = new GenericModrinthClient({
1717
* userAgent: 'my-app/1.0.0',
1818
* features: [
19-
* new AuthFeature({ token: 'mrp_...' }),
19+
* new AuthFeature({ token: async () => getOAuthToken() }),
2020
* new RetryFeature({ maxAttempts: 3 })
2121
* ]
2222
* })

‎packages/api-client/src/platform/nuxt.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,17 @@ export interface NuxtClientConfig extends ClientConfig {
6666
* ```typescript
6767
* // In a Nuxt composable
6868
* const config = useRuntimeConfig()
69-
* const auth = await useAuth()
7069
*
7170
* const client = new NuxtModrinthClient({
7271
* userAgent: 'my-nuxt-app/1.0.0',
7372
* rateLimitKey: import.meta.server ? config.rateLimitKey : undefined,
7473
* features: [
75-
* new AuthFeature({ token: () => auth.value.token })
74+
* new AuthFeature({
75+
* token: async () => getOAuthToken()
76+
* }),
77+
* new CircuitBreakerFeature({
78+
* storage: new NuxtCircuitBreakerStorage()
79+
* })
7680
* ]
7781
* })
7882
*
@@ -171,9 +175,9 @@ export class NuxtModrinthClient extends XHRUploadClient {
171175
return super.normalizeError(error)
172176
}
173177

174-
protected buildDefaultHeaders(): Record<string, string> {
178+
protected async buildDefaultHeaders(): Promise<Record<string, string>> {
175179
const headers: Record<string, string> = {
176-
...super.buildDefaultHeaders(),
180+
...(await super.buildDefaultHeaders()),
177181
}
178182

179183
// Use the resolved key (populated by resolveRateLimitKey in request())

‎packages/api-client/src/platform/tauri.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ interface HttpError extends Error {
2727
* ```typescript
2828
* import { getVersion } from '@tauri-apps/api/app'
2929
*
30-
* const version = await getVersion()
3130
* const client = new TauriModrinthClient({
32-
* userAgent: `modrinth/theseus/${version} (support@modrinth.com)`,
31+
* userAgent: async () => `modrinth/theseus/${await getVersion()} (support@modrinth.com)`,
3332
* features: [
34-
* new AuthFeature({ token: 'mrp_...' })
33+
* new AuthFeature({ token: async () => getOAuthToken() })
3534
* ]
3635
* })
3736
*

‎packages/api-client/src/platform/xhr-upload-client.ts‎

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,50 +27,57 @@ export abstract class XHRUploadClient extends AbstractModrinthClient {
2727

2828
const url = this.buildUrl(path, baseUrl, options.version)
2929

30-
// For FormData uploads, don't set Content-Type (let browser set multipart boundary)
31-
// For file uploads, use application/octet-stream
32-
const isFormData = 'formData' in options && options.formData instanceof FormData
33-
const baseHeaders = this.buildDefaultHeaders()
34-
// Remove Content-Type for FormData so browser can set multipart/form-data with boundary
35-
if (isFormData) {
36-
delete baseHeaders['Content-Type']
37-
} else {
38-
baseHeaders['Content-Type'] = 'application/octet-stream'
39-
}
40-
41-
const mergedOptions: UploadRequestOptions = {
42-
retry: false, // default: don't retry uploads
43-
...options,
44-
headers: {
45-
...baseHeaders,
46-
...options.headers,
47-
},
48-
}
49-
this.attachArchonSentryCaptureHeader(mergedOptions)
50-
51-
const context = this.buildUploadContext(url, path, mergedOptions)
52-
5330
const progressCallbacks: Array<(p: UploadProgress) => void> = []
54-
if (mergedOptions.onProgress) {
55-
progressCallbacks.push(mergedOptions.onProgress)
31+
if (options.onProgress) {
32+
progressCallbacks.push(options.onProgress)
5633
}
5734
const abortController = new AbortController()
5835

59-
if (mergedOptions.signal) {
60-
mergedOptions.signal.addEventListener('abort', () => abortController.abort())
36+
if (options.signal) {
37+
options.signal.addEventListener('abort', () => abortController.abort())
6138
}
6239

40+
let context: RequestContext | undefined
6341
const handle: UploadHandle<T> = {
64-
promise: this.executeUploadFeatureChain<T>(context, progressCallbacks, abortController)
65-
.then(async (result) => {
66-
await this.config.hooks?.onResponse?.(result, context)
67-
return result
68-
})
69-
.catch(async (error) => {
70-
const apiError = this.normalizeError(error, context)
42+
promise: (async () => {
43+
const isFormData = 'formData' in options && options.formData instanceof FormData
44+
const baseHeaders = await this.buildDefaultHeaders()
45+
if (isFormData) {
46+
delete baseHeaders['Content-Type']
47+
} else {
48+
baseHeaders['Content-Type'] = 'application/octet-stream'
49+
}
50+
51+
const mergedOptions: UploadRequestOptions = {
52+
retry: false,
53+
...options,
54+
headers: {
55+
...baseHeaders,
56+
...options.headers,
57+
},
58+
}
59+
this.attachArchonSentryCaptureHeader(mergedOptions)
60+
61+
const uploadContext = this.buildUploadContext(url, path, mergedOptions)
62+
context = uploadContext
63+
if (abortController.signal.aborted) {
64+
throw new ModrinthApiError('Upload cancelled')
65+
}
66+
67+
const result = await this.executeUploadFeatureChain<T>(
68+
uploadContext,
69+
progressCallbacks,
70+
abortController,
71+
)
72+
await this.config.hooks?.onResponse?.(result, uploadContext)
73+
return result
74+
})().catch(async (error) => {
75+
const apiError = this.normalizeError(error, context)
76+
if (context) {
7177
await this.config.hooks?.onError?.(apiError, context)
72-
throw apiError
73-
}),
78+
}
79+
throw apiError
80+
}),
7481
onProgress: (callback) => {
7582
progressCallbacks.push(callback)
7683
return handle

‎packages/api-client/src/types/client.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { AbstractFeature } from '../core/abstract-feature'
22
import type { RequestContext } from './request'
33

4+
export type MaybePromise<T> = T | Promise<T>
5+
export type UserAgentProvider = string | (() => MaybePromise<string | undefined>)
6+
47
/**
58
* Request lifecycle hooks
69
*/
@@ -26,11 +29,11 @@ export type RequestHooks = {
2629
*/
2730
export interface ClientConfig {
2831
/**
29-
* User agent string for requests
32+
* User agent string or provider for requests
3033
* Should identify your application (e.g., 'my-app/1.0.0')
3134
* If not provided, the platform's default user agent will be used
3235
*/
33-
userAgent?: string
36+
userAgent?: UserAgentProvider
3437

3538
/**
3639
* Base URL for Labrinth API (main Modrinth API)

0 commit comments

Comments
 (0)