fix: app user agent for api-client reqs using tauri http plugin (#6045)
fix: app user agent
This commit is contained in:
@@ -146,8 +146,9 @@ const popupNotificationManager = new AppPopupNotificationManager()
|
|||||||
providePopupNotificationManager(popupNotificationManager)
|
providePopupNotificationManager(popupNotificationManager)
|
||||||
const { addPopupNotification } = popupNotificationManager
|
const { addPopupNotification } = popupNotificationManager
|
||||||
|
|
||||||
|
const appVersion = getVersion()
|
||||||
const tauriApiClient = new TauriModrinthClient({
|
const tauriApiClient = new TauriModrinthClient({
|
||||||
userAgent: `modrinth/theseus/${getVersion()} (support@modrinth.com)`,
|
userAgent: async () => `modrinth/theseus/${await appVersion} (support@modrinth.com)`,
|
||||||
labrinthBaseUrl: config.labrinthBaseUrl,
|
labrinthBaseUrl: config.labrinthBaseUrl,
|
||||||
archonBaseUrl: config.archonBaseUrl,
|
archonBaseUrl: config.archonBaseUrl,
|
||||||
features: [
|
features: [
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { AuthFeature, GenericModrinthClient, type Labrinth } from '@modrinth/api
|
|||||||
|
|
||||||
const client = new GenericModrinthClient({
|
const client = new GenericModrinthClient({
|
||||||
userAgent: 'my-app/1.0.0',
|
userAgent: 'my-app/1.0.0',
|
||||||
features: [new AuthFeature({ token: 'mrp_...' })],
|
features: [new AuthFeature({ token: process.env.MODRINTH_TOKEN })],
|
||||||
})
|
})
|
||||||
|
|
||||||
const project: Labrinth.Projects.v2.Project = await client.labrinth.projects_v2.get('sodium')
|
const project: Labrinth.Projects.v2.Project = await client.labrinth.projects_v2.get('sodium')
|
||||||
@@ -51,14 +51,13 @@ import { AuthFeature, CircuitBreakerFeature, NuxtCircuitBreakerStorage, NuxtModr
|
|||||||
|
|
||||||
export const useModrinthClient = async () => {
|
export const useModrinthClient = async () => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const auth = await useAuth()
|
|
||||||
|
|
||||||
return new NuxtModrinthClient({
|
return new NuxtModrinthClient({
|
||||||
userAgent: 'my-nuxt-app/1.0.0',
|
userAgent: 'my-nuxt-app/1.0.0',
|
||||||
rateLimitKey: import.meta.server ? config.rateLimitKey : undefined,
|
rateLimitKey: import.meta.server ? config.rateLimitKey : undefined,
|
||||||
features: [
|
features: [
|
||||||
new AuthFeature({
|
new AuthFeature({
|
||||||
token: async () => auth.value.token,
|
token: process.env.MODRINTH_TOKEN,
|
||||||
}),
|
}),
|
||||||
new CircuitBreakerFeature({
|
new CircuitBreakerFeature({
|
||||||
storage: new NuxtCircuitBreakerStorage(),
|
storage: new NuxtCircuitBreakerStorage(),
|
||||||
@@ -74,10 +73,9 @@ export const useModrinthClient = async () => {
|
|||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
import { AuthFeature, TauriModrinthClient } from '@modrinth/api-client'
|
import { AuthFeature, TauriModrinthClient } from '@modrinth/api-client'
|
||||||
|
|
||||||
const version = await getVersion()
|
|
||||||
const client = new TauriModrinthClient({
|
const client = new TauriModrinthClient({
|
||||||
userAgent: `modrinth/theseus/${version} (support@modrinth.com)`,
|
userAgent: async () => `modrinth/theseus/${await getVersion()} (support@modrinth.com)`,
|
||||||
features: [new AuthFeature({ token: 'mrp_...' })],
|
features: [new AuthFeature({ token: process.env.MODRINTH_TOKEN })],
|
||||||
})
|
})
|
||||||
|
|
||||||
const project = await client.labrinth.projects_v2.get('sodium')
|
const project = await client.labrinth.projects_v2.get('sodium')
|
||||||
@@ -138,7 +136,7 @@ Features wrap requests before they reach the platform implementation:
|
|||||||
import { AuthFeature, CircuitBreakerFeature, RetryFeature } from '@modrinth/api-client'
|
import { AuthFeature, CircuitBreakerFeature, RetryFeature } from '@modrinth/api-client'
|
||||||
|
|
||||||
const client = new GenericModrinthClient({
|
const client = new GenericModrinthClient({
|
||||||
features: [new AuthFeature({ token: async () => getToken() }), new RetryFeature({ maxAttempts: 3, backoffStrategy: 'exponential' }), new CircuitBreakerFeature({ maxFailures: 3, resetTimeout: 30_000 })],
|
features: [new AuthFeature({ token: async () => process.env.MODRINTH_TOKEN }), new RetryFeature({ maxAttempts: 3, backoffStrategy: 'exponential' }), new CircuitBreakerFeature({ maxFailures: 3, resetTimeout: 30_000 })],
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -125,13 +125,15 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
|||||||
|
|
||||||
const url = this.buildUrl(path, baseUrl, options.version)
|
const url = this.buildUrl(path, baseUrl, options.version)
|
||||||
|
|
||||||
|
const defaultHeaders = await this.buildDefaultHeaders()
|
||||||
|
|
||||||
// Merge options with defaults
|
// Merge options with defaults
|
||||||
const mergedOptions: RequestOptions = {
|
const mergedOptions: RequestOptions = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
timeout: this.config.timeout,
|
timeout: this.config.timeout,
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
...this.buildDefaultHeaders(),
|
...defaultHeaders,
|
||||||
...options.headers,
|
...options.headers,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -306,19 +308,25 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
|||||||
* Subclasses can override this to add platform-specific headers
|
* Subclasses can override this to add platform-specific headers
|
||||||
* (e.g., Nuxt rate limit key)
|
* (e.g., Nuxt rate limit key)
|
||||||
*/
|
*/
|
||||||
protected buildDefaultHeaders(): Record<string, string> {
|
protected async buildDefaultHeaders(): Promise<Record<string, string>> {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...this.config.headers,
|
...this.config.headers,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.userAgent) {
|
const userAgent = await this.resolveUserAgent()
|
||||||
headers['User-Agent'] = this.config.userAgent
|
if (userAgent) {
|
||||||
|
headers['User-Agent'] = userAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveUserAgent(): Promise<string | undefined> {
|
||||||
|
const userAgent = this.config.userAgent
|
||||||
|
return typeof userAgent === 'function' ? await userAgent() : userAgent
|
||||||
|
}
|
||||||
|
|
||||||
protected attachArchonSentryCaptureHeader(options: RequestOptions): void {
|
protected attachArchonSentryCaptureHeader(options: RequestOptions): void {
|
||||||
if (options.api !== 'archon' || !options.headers || !this.shouldCaptureArchonRequests()) {
|
if (options.api !== 'archon' || !options.headers || !this.shouldCaptureArchonRequests()) {
|
||||||
return
|
return
|
||||||
@@ -404,7 +412,7 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
|||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* const client = new GenericModrinthClient()
|
* const client = new GenericModrinthClient()
|
||||||
* client.addFeature(new AuthFeature({ token: 'mrp_...' }))
|
* client.addFeature(new AuthFeature({ token: async () => getOAuthToken() }))
|
||||||
* client.addFeature(new RetryFeature({ maxAttempts: 3 }))
|
* client.addFeature(new RetryFeature({ maxAttempts: 3 }))
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -33,14 +33,8 @@ export interface AuthConfig extends FeatureConfig {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // Static token
|
|
||||||
* const auth = new AuthFeature({
|
* const auth = new AuthFeature({
|
||||||
* token: 'mrp_...'
|
* token: async () => process.env.MODRINTH_TOKEN
|
||||||
* })
|
|
||||||
*
|
|
||||||
* // Dynamic token (e.g., from auth state)
|
|
||||||
* const auth = new AuthFeature({
|
|
||||||
* token: async () => await getAuthToken()
|
|
||||||
* })
|
* })
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { XHRUploadClient } from './xhr-upload-client'
|
|||||||
* const client = new GenericModrinthClient({
|
* const client = new GenericModrinthClient({
|
||||||
* userAgent: 'my-app/1.0.0',
|
* userAgent: 'my-app/1.0.0',
|
||||||
* features: [
|
* features: [
|
||||||
* new AuthFeature({ token: 'mrp_...' }),
|
* new AuthFeature({ token: async () => getOAuthToken() }),
|
||||||
* new RetryFeature({ maxAttempts: 3 })
|
* new RetryFeature({ maxAttempts: 3 })
|
||||||
* ]
|
* ]
|
||||||
* })
|
* })
|
||||||
|
|||||||
@@ -66,13 +66,17 @@ export interface NuxtClientConfig extends ClientConfig {
|
|||||||
* ```typescript
|
* ```typescript
|
||||||
* // In a Nuxt composable
|
* // In a Nuxt composable
|
||||||
* const config = useRuntimeConfig()
|
* const config = useRuntimeConfig()
|
||||||
* const auth = await useAuth()
|
|
||||||
*
|
*
|
||||||
* const client = new NuxtModrinthClient({
|
* const client = new NuxtModrinthClient({
|
||||||
* userAgent: 'my-nuxt-app/1.0.0',
|
* userAgent: 'my-nuxt-app/1.0.0',
|
||||||
* rateLimitKey: import.meta.server ? config.rateLimitKey : undefined,
|
* rateLimitKey: import.meta.server ? config.rateLimitKey : undefined,
|
||||||
* features: [
|
* features: [
|
||||||
* new AuthFeature({ token: () => auth.value.token })
|
* new AuthFeature({
|
||||||
|
* token: async () => getOAuthToken()
|
||||||
|
* }),
|
||||||
|
* new CircuitBreakerFeature({
|
||||||
|
* storage: new NuxtCircuitBreakerStorage()
|
||||||
|
* })
|
||||||
* ]
|
* ]
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
@@ -171,9 +175,9 @@ export class NuxtModrinthClient extends XHRUploadClient {
|
|||||||
return super.normalizeError(error)
|
return super.normalizeError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildDefaultHeaders(): Record<string, string> {
|
protected async buildDefaultHeaders(): Promise<Record<string, string>> {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
...super.buildDefaultHeaders(),
|
...(await super.buildDefaultHeaders()),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the resolved key (populated by resolveRateLimitKey in request())
|
// Use the resolved key (populated by resolveRateLimitKey in request())
|
||||||
|
|||||||
@@ -27,11 +27,10 @@ interface HttpError extends Error {
|
|||||||
* ```typescript
|
* ```typescript
|
||||||
* import { getVersion } from '@tauri-apps/api/app'
|
* import { getVersion } from '@tauri-apps/api/app'
|
||||||
*
|
*
|
||||||
* const version = await getVersion()
|
|
||||||
* const client = new TauriModrinthClient({
|
* const client = new TauriModrinthClient({
|
||||||
* userAgent: `modrinth/theseus/${version} (support@modrinth.com)`,
|
* userAgent: async () => `modrinth/theseus/${await getVersion()} (support@modrinth.com)`,
|
||||||
* features: [
|
* features: [
|
||||||
* new AuthFeature({ token: 'mrp_...' })
|
* new AuthFeature({ token: async () => getOAuthToken() })
|
||||||
* ]
|
* ]
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -27,50 +27,57 @@ export abstract class XHRUploadClient extends AbstractModrinthClient {
|
|||||||
|
|
||||||
const url = this.buildUrl(path, baseUrl, options.version)
|
const url = this.buildUrl(path, baseUrl, options.version)
|
||||||
|
|
||||||
// For FormData uploads, don't set Content-Type (let browser set multipart boundary)
|
|
||||||
// For file uploads, use application/octet-stream
|
|
||||||
const isFormData = 'formData' in options && options.formData instanceof FormData
|
|
||||||
const baseHeaders = this.buildDefaultHeaders()
|
|
||||||
// Remove Content-Type for FormData so browser can set multipart/form-data with boundary
|
|
||||||
if (isFormData) {
|
|
||||||
delete baseHeaders['Content-Type']
|
|
||||||
} else {
|
|
||||||
baseHeaders['Content-Type'] = 'application/octet-stream'
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedOptions: UploadRequestOptions = {
|
|
||||||
retry: false, // default: don't retry uploads
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
...baseHeaders,
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
this.attachArchonSentryCaptureHeader(mergedOptions)
|
|
||||||
|
|
||||||
const context = this.buildUploadContext(url, path, mergedOptions)
|
|
||||||
|
|
||||||
const progressCallbacks: Array<(p: UploadProgress) => void> = []
|
const progressCallbacks: Array<(p: UploadProgress) => void> = []
|
||||||
if (mergedOptions.onProgress) {
|
if (options.onProgress) {
|
||||||
progressCallbacks.push(mergedOptions.onProgress)
|
progressCallbacks.push(options.onProgress)
|
||||||
}
|
}
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
|
|
||||||
if (mergedOptions.signal) {
|
if (options.signal) {
|
||||||
mergedOptions.signal.addEventListener('abort', () => abortController.abort())
|
options.signal.addEventListener('abort', () => abortController.abort())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let context: RequestContext | undefined
|
||||||
const handle: UploadHandle<T> = {
|
const handle: UploadHandle<T> = {
|
||||||
promise: this.executeUploadFeatureChain<T>(context, progressCallbacks, abortController)
|
promise: (async () => {
|
||||||
.then(async (result) => {
|
const isFormData = 'formData' in options && options.formData instanceof FormData
|
||||||
await this.config.hooks?.onResponse?.(result, context)
|
const baseHeaders = await this.buildDefaultHeaders()
|
||||||
return result
|
if (isFormData) {
|
||||||
})
|
delete baseHeaders['Content-Type']
|
||||||
.catch(async (error) => {
|
} else {
|
||||||
const apiError = this.normalizeError(error, context)
|
baseHeaders['Content-Type'] = 'application/octet-stream'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedOptions: UploadRequestOptions = {
|
||||||
|
retry: false,
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...baseHeaders,
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
this.attachArchonSentryCaptureHeader(mergedOptions)
|
||||||
|
|
||||||
|
const uploadContext = this.buildUploadContext(url, path, mergedOptions)
|
||||||
|
context = uploadContext
|
||||||
|
if (abortController.signal.aborted) {
|
||||||
|
throw new ModrinthApiError('Upload cancelled')
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.executeUploadFeatureChain<T>(
|
||||||
|
uploadContext,
|
||||||
|
progressCallbacks,
|
||||||
|
abortController,
|
||||||
|
)
|
||||||
|
await this.config.hooks?.onResponse?.(result, uploadContext)
|
||||||
|
return result
|
||||||
|
})().catch(async (error) => {
|
||||||
|
const apiError = this.normalizeError(error, context)
|
||||||
|
if (context) {
|
||||||
await this.config.hooks?.onError?.(apiError, context)
|
await this.config.hooks?.onError?.(apiError, context)
|
||||||
throw apiError
|
}
|
||||||
}),
|
throw apiError
|
||||||
|
}),
|
||||||
onProgress: (callback) => {
|
onProgress: (callback) => {
|
||||||
progressCallbacks.push(callback)
|
progressCallbacks.push(callback)
|
||||||
return handle
|
return handle
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { AbstractFeature } from '../core/abstract-feature'
|
import type { AbstractFeature } from '../core/abstract-feature'
|
||||||
import type { RequestContext } from './request'
|
import type { RequestContext } from './request'
|
||||||
|
|
||||||
|
export type MaybePromise<T> = T | Promise<T>
|
||||||
|
export type UserAgentProvider = string | (() => MaybePromise<string | undefined>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request lifecycle hooks
|
* Request lifecycle hooks
|
||||||
*/
|
*/
|
||||||
@@ -26,11 +29,11 @@ export type RequestHooks = {
|
|||||||
*/
|
*/
|
||||||
export interface ClientConfig {
|
export interface ClientConfig {
|
||||||
/**
|
/**
|
||||||
* User agent string for requests
|
* User agent string or provider for requests
|
||||||
* Should identify your application (e.g., 'my-app/1.0.0')
|
* Should identify your application (e.g., 'my-app/1.0.0')
|
||||||
* If not provided, the platform's default user agent will be used
|
* If not provided, the platform's default user agent will be used
|
||||||
*/
|
*/
|
||||||
userAgent?: string
|
userAgent?: UserAgentProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base URL for Labrinth API (main Modrinth API)
|
* Base URL for Labrinth API (main Modrinth API)
|
||||||
|
|||||||
Reference in New Issue
Block a user