feat: replace axios with direct fetch usage
This commit is contained in:
parent
4f78221286
commit
ee8bd6c3c9
|
|
@ -79,7 +79,6 @@
|
|||
"@tiptap/vue-3": "2.26.1",
|
||||
"@vueuse/core": "13.5.0",
|
||||
"@vueuse/router": "13.5.0",
|
||||
"axios": "1.10.0",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"change-case": "5.4.4",
|
||||
|
|
|
|||
|
|
@ -109,9 +109,6 @@ importers:
|
|||
'@vueuse/router':
|
||||
specifier: 13.5.0
|
||||
version: 13.5.0(vue-router@4.5.1(vue@3.5.17(typescript@5.8.3)))(vue@3.5.17(typescript@5.8.3))
|
||||
axios:
|
||||
specifier: 1.10.0
|
||||
version: 1.10.0(debug@4.4.1)
|
||||
blurhash:
|
||||
specifier: 2.0.5
|
||||
version: 2.0.5
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {AuthenticatedHTTPFactory} from '@/helpers/fetcher'
|
||||
import type {AxiosResponse} from 'axios'
|
||||
import {AuthenticatedHTTPFactory, type HttpResponse} from '@/helpers/fetcher'
|
||||
|
||||
let savedToken: string | null = null
|
||||
|
||||
|
|
@ -37,7 +36,7 @@ export const removeToken = () => {
|
|||
/**
|
||||
* Refreshes an auth token while ensuring it is updated everywhere.
|
||||
*/
|
||||
export async function refreshToken(persist: boolean): Promise<AxiosResponse> {
|
||||
export async function refreshToken(persist: boolean): Promise<HttpResponse> {
|
||||
const HTTP = AuthenticatedHTTPFactory()
|
||||
try {
|
||||
const response = await HTTP.post('user/token')
|
||||
|
|
|
|||
|
|
@ -1,36 +1,239 @@
|
|||
import axios from 'axios'
|
||||
import {getToken} from '@/helpers/auth'
|
||||
|
||||
export type Method = 'GET' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'POST' | 'PUT' | 'PATCH'
|
||||
|
||||
export interface RequestConfig {
|
||||
url: string
|
||||
method?: Method
|
||||
headers?: Record<string, string>
|
||||
params?: Record<string, unknown>
|
||||
data?: unknown
|
||||
baseURL?: string
|
||||
responseType?: 'json' | 'blob' | 'text'
|
||||
onUploadProgress?: (progress: {progress: number}) => void
|
||||
transformRequest?: (data: unknown) => unknown
|
||||
}
|
||||
|
||||
export interface HttpResponse<T = unknown> {
|
||||
data: T
|
||||
headers: Record<string, string>
|
||||
status: number
|
||||
}
|
||||
|
||||
export class HttpError extends Error {
|
||||
status?: number
|
||||
data?: unknown
|
||||
response?: Response
|
||||
}
|
||||
|
||||
class InterceptorManager<T> {
|
||||
private handlers: Array<(arg: T) => Promise<T> | T> = []
|
||||
|
||||
use(handler: (arg: T) => Promise<T> | T) {
|
||||
this.handlers.push(handler)
|
||||
}
|
||||
|
||||
async run(arg: T): Promise<T> {
|
||||
for (const handler of this.handlers) {
|
||||
arg = await handler(arg)
|
||||
}
|
||||
return arg
|
||||
}
|
||||
}
|
||||
|
||||
class HttpClient {
|
||||
private baseURL: string
|
||||
interceptors = {
|
||||
request: new InterceptorManager<RequestConfig>(),
|
||||
response: new InterceptorManager<HttpResponse>(),
|
||||
}
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.baseURL = baseURL
|
||||
}
|
||||
|
||||
private buildUrl(config: RequestConfig) {
|
||||
let url = config.url
|
||||
if (!url.startsWith('http')) {
|
||||
url = (config.baseURL ?? this.baseURL ?? '') + url
|
||||
}
|
||||
if (config.params) {
|
||||
const qs = new URLSearchParams(config.params as Record<string, string>).toString()
|
||||
if (qs) {
|
||||
url += (url.includes('?') ? '&' : '?') + qs
|
||||
}
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
private parseHeaders(headers: Headers): Record<string, string> {
|
||||
const result: Record<string, string> = {}
|
||||
headers.forEach((v, k) => {
|
||||
result[k] = v
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
private prepareRequestBody(data: unknown, transformRequest?: (data: unknown) => unknown): unknown {
|
||||
let body = data
|
||||
if (transformRequest) {
|
||||
body = transformRequest(body)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
private async parseResponseData(response: Response, responseType?: string): Promise<unknown> {
|
||||
if (responseType === 'blob') {
|
||||
return await response.blob()
|
||||
}
|
||||
|
||||
if (responseType === 'text') {
|
||||
return await response.text()
|
||||
}
|
||||
|
||||
try {
|
||||
return await response.json()
|
||||
} catch {
|
||||
return await response.text()
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchRequest(config: RequestConfig): Promise<HttpResponse> {
|
||||
const url = this.buildUrl(config)
|
||||
const init: RequestInit = {method: config.method, headers: config.headers}
|
||||
|
||||
const body = this.prepareRequestBody(config.data, config.transformRequest)
|
||||
|
||||
if (typeof body !== 'undefined') {
|
||||
if (body instanceof FormData || body instanceof Blob) {
|
||||
init.body = body as BodyInit
|
||||
} else if (typeof body === 'string') {
|
||||
init.body = body
|
||||
} else {
|
||||
init.body = JSON.stringify(body)
|
||||
if (init.headers && !(init.headers as Record<string, string>)['Content-Type']) {
|
||||
(init.headers as Record<string, string>)['Content-Type'] = 'application/json'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, init)
|
||||
return {
|
||||
status: response.status,
|
||||
headers: this.parseHeaders(response.headers),
|
||||
data: await this.parseResponseData(response, config.responseType),
|
||||
}
|
||||
}
|
||||
|
||||
private xhrRequest(config: RequestConfig): Promise<HttpResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = this.buildUrl(config)
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open(config.method ?? 'GET', url)
|
||||
if (config.headers) {
|
||||
for (const [k, v] of Object.entries(config.headers)) {
|
||||
xhr.setRequestHeader(k, v)
|
||||
}
|
||||
}
|
||||
if (config.responseType === 'blob') {
|
||||
xhr.responseType = 'blob'
|
||||
}
|
||||
|
||||
xhr.upload.onprogress = ev => {
|
||||
if (config.onUploadProgress && ev.lengthComputable) {
|
||||
config.onUploadProgress({progress: ev.loaded / ev.total})
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onload = () => {
|
||||
const headers: Record<string, string> = {}
|
||||
const raw = xhr.getAllResponseHeaders().trim().split(/\r?\n/)
|
||||
for (const line of raw) {
|
||||
const parts = line.split(': ')
|
||||
headers[parts.shift()!.toLowerCase()] = parts.join(': ')
|
||||
}
|
||||
let data: unknown = xhr.response
|
||||
if (config.responseType === 'blob') {
|
||||
data = xhr.response
|
||||
} else {
|
||||
try {
|
||||
data = JSON.parse(xhr.responseText)
|
||||
} catch {
|
||||
data = xhr.responseText
|
||||
}
|
||||
}
|
||||
resolve({status: xhr.status, headers, data})
|
||||
}
|
||||
|
||||
xhr.onerror = () => {
|
||||
const err = new HttpError('Network Error')
|
||||
reject(err)
|
||||
}
|
||||
|
||||
const body = this.prepareRequestBody(config.data, config.transformRequest)
|
||||
if (body instanceof FormData || body instanceof Blob) {
|
||||
xhr.send(body as BodyInit)
|
||||
} else if (typeof body === 'string' || typeof body === 'undefined') {
|
||||
xhr.send(body)
|
||||
} else {
|
||||
xhr.send(JSON.stringify(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async request(config: RequestConfig): Promise<HttpResponse> {
|
||||
config = await this.interceptors.request.run({...config})
|
||||
let response: HttpResponse
|
||||
if (config.onUploadProgress) {
|
||||
response = await this.xhrRequest(config)
|
||||
} else {
|
||||
response = await this.fetchRequest(config)
|
||||
}
|
||||
response = await this.interceptors.response.run(response)
|
||||
if (response.status >= 400) {
|
||||
const err = new HttpError('Request failed with status ' + response.status)
|
||||
err.status = response.status
|
||||
err.data = response.data
|
||||
throw err
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
get(url: string, config: Partial<RequestConfig> = {}) {
|
||||
return this.request({...config, url, method: 'GET'})
|
||||
}
|
||||
|
||||
delete(url: string, data?: unknown, config: Partial<RequestConfig> = {}) {
|
||||
return this.request({...config, url, method: 'DELETE', data})
|
||||
}
|
||||
|
||||
post(url: string, data?: unknown, config: Partial<RequestConfig> = {}) {
|
||||
return this.request({...config, url, method: 'POST', data})
|
||||
}
|
||||
|
||||
put(url: string, data?: unknown, config: Partial<RequestConfig> = {}) {
|
||||
return this.request({...config, url, method: 'PUT', data})
|
||||
}
|
||||
}
|
||||
|
||||
export function HTTPFactory() {
|
||||
const instance = axios.create({baseURL: window.API_URL})
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
// by setting the baseURL fresh for every request
|
||||
// we make sure that it is never outdated in case it is updated
|
||||
config.baseURL = window.API_URL
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
return instance
|
||||
return new HttpClient(window.API_URL)
|
||||
}
|
||||
|
||||
export function AuthenticatedHTTPFactory() {
|
||||
export function AuthenticatedHTTPFactory() {
|
||||
const instance = HTTPFactory()
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
// Set the default auth header if we have a token
|
||||
instance.interceptors.request.use(config => {
|
||||
const token = getToken()
|
||||
if (token !== null) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
config.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...config.headers,
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
export type FetchHttpInstance = HttpClient
|
||||
|
||||
export default HttpClient
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'virtual:vite-plugin-sentry/sentry-config'
|
||||
import type {App} from 'vue'
|
||||
import type {Router} from 'vue-router'
|
||||
import {AxiosError} from 'axios'
|
||||
import {HttpError} from '@/helpers/fetcher'
|
||||
|
||||
export default async function setupSentry(app: App, router: Router) {
|
||||
const Sentry = await import('@sentry/vue')
|
||||
|
|
@ -21,9 +21,10 @@ export default async function setupSentry(app: App, router: Router) {
|
|||
tracesSampleRate: 1.0,
|
||||
beforeSend(event, hint) {
|
||||
|
||||
if ((typeof hint.originalException?.code !== 'undefined' &&
|
||||
typeof hint.originalException?.message !== 'undefined')
|
||||
|| hint.originalException instanceof AxiosError) {
|
||||
if (
|
||||
(typeof hint.originalException?.code !== 'undefined' && typeof hint.originalException?.message !== 'undefined')
|
||||
|| hint.originalException instanceof HttpError
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {AuthenticatedHTTPFactory} from '@/helpers/fetcher'
|
||||
import type {Method} from 'axios'
|
||||
import {AuthenticatedHTTPFactory, type Method} from '@/helpers/fetcher'
|
||||
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
import AbstractModel from '@/models/abstractModel'
|
||||
|
|
@ -74,7 +73,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
// Set the interceptors to process every request
|
||||
this.http.interceptors.request.use((config) => {
|
||||
switch (config.method) {
|
||||
case 'post':
|
||||
case 'POST':
|
||||
if (this.useUpdateInterceptor()) {
|
||||
config.data = this.beforeUpdate(config.data)
|
||||
if(this.autoTransformBeforePost()) {
|
||||
|
|
@ -82,7 +81,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
}
|
||||
}
|
||||
break
|
||||
case 'put':
|
||||
case 'PUT':
|
||||
if (this.useCreateInterceptor()) {
|
||||
config.data = this.beforeCreate(config.data)
|
||||
if(this.autoTransformBeforePut()) {
|
||||
|
|
@ -90,7 +89,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
}
|
||||
}
|
||||
break
|
||||
case 'delete':
|
||||
case 'DELETE':
|
||||
if (this.useDeleteInterceptor()) {
|
||||
config.data = this.beforeDelete(config.data)
|
||||
if(this.autoTransformBeforeDelete()) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue