• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / iznik-nuxt3 / c116036c-55fb-4d4e-a732-06fbe0906fc1

13 Oct 2025 12:31PM UTC coverage: 34.681% (-11.0%) from 45.694%
c116036c-55fb-4d4e-a732-06fbe0906fc1

push

circleci

edwh
Migrate logo API call from v1 to v2

1053 of 3928 branches covered (26.81%)

Branch coverage included in aggregate %.

0 of 1 new or added line in 1 file covered. (0.0%)

723 existing lines in 55 files now uncovered.

2722 of 6957 relevant lines covered (39.13%)

38.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

58.44
/api/BaseAPI.js
1
import * as Sentry from '@sentry/browser'
2
import { fetchRetry } from '~/composables/useFetchRetry'
3
import { useAuthStore } from '~/stores/auth'
4
import { useMiscStore } from '~/stores/misc'
5

6
let requestId = 0
6✔
7

8
// let timer = 0
9

10
// We add fetch retrying.
11
//
12
// Note that $fetch and useFetch cause problems on Node v18, so we don't use them.
13
const ourFetch = fetchRetry(fetch)
6✔
14
export class APIError extends Error {
6✔
15
  constructor({ request, response }, message) {
16
    super(message)
3✔
17
    Object.assign(this, { request, response })
3✔
18
  }
19
}
20

21
export class MaintenanceError extends Error {
6✔
22
  constructor({ request, response }, message) {
23
    super(message)
×
24
    Object.assign(this, { request, response })
×
25
  }
26
}
27

28
export class LoginError extends Error {
6✔
29
  constructor(ret, status) {
30
    super(status)
3✔
31
    Object.assign(this, { ret, status })
3✔
32
  }
33
}
34

35
export class SignUpError extends Error {
6✔
36
  constructor(ret, status) {
37
    super(status)
×
38
    Object.assign(this, { ret, status })
×
39
  }
40
}
41

42
export default class BaseAPI {
6✔
43
  constructor(config) {
44
    this.config = config
25,748✔
45
  }
46

47
  async $request(method, path, config, logError = true, body = null) {
654✔
48
    let status = null
327✔
49
    let data = null
327✔
50

51
    try {
327✔
52
      const headers = config.headers ? config.headers : {}
327✔
53

54
      const authStore = useAuthStore()
327✔
55
      const miscStore = useMiscStore()
327✔
56

57
      if (authStore.auth.persistent) {
327✔
58
        // Use the persistent token (a kind of JWT) to authenticate the request.
59
        headers.Authorization =
127✔
60
          'Iznik ' + JSON.stringify(authStore.auth.persistent)
61
      }
62

63
      const loggedInAs = authStore.user?.id
327✔
64

65
      if (loggedInAs) {
327✔
66
        // Add the user ID as a query parameter to the path, checking whether there already are any
67
        // query parameters.
68
        path += (path.includes('?') ? '&' : '?') + 'loggedInAs=' + loggedInAs
121!
69
      }
70

71
      // Add requestId to the path, checking whether there already are any query parameters.
72
      path += (path.includes('?') ? '&' : '?') + 'requestid=' + requestId++
327✔
73

74
      headers['Cache-Control'] =
327✔
75
        'max-age=0, must-revalidate, no-cache, no-store, private'
76

77
      if (method === 'GET' && config?.params) {
327✔
78
        // Remove falsey values from the params - unless MT needs to send a zero
79
        if (!config?.params.dontzapfalsey) {
79!
80
          config.params = Object.fromEntries(
79✔
81
            Object.entries(config.params).filter(([_, v]) => v)
82
          )
83
        }
84
        config.params.modtools = miscStore.modtools
79✔
85

86
        // MT cope with arrays and objects eg components: ['me','work'] or context: { "Added": 12345678, "id": 12345 }
87
        Object.keys(config.params).forEach((c) => {
79✔
88
          const v = config.params[c]
190✔
89
          if (Array.isArray(v)) {
190✔
90
            delete config.params[c]
42✔
91
            for (let ix = 0; ix < v.length; ix++) {
42✔
92
              config.params[c + '[' + ix + ']'] = v[ix]
42✔
93
            }
94
          } else if (typeof v === 'object' && v !== null) {
148!
95
            delete config.params[c]
×
96
            Object.keys(v).forEach((cp) => {
×
97
              config.params[c + '[' + cp + ']'] = v[cp]
×
98
            })
99
          }
100
        })
101
        // URL encode the parameters if any
102
        const urlParams = new URLSearchParams(config.params).toString()
79✔
103

104
        if (urlParams.length) {
79!
105
          path += '&' + urlParams
79✔
106
        }
107
      } else if (method !== 'POST') {
248!
108
        // Any parameters are passed in config.params.
109
        if (!config?.params) {
×
110
          config.params = {}
×
111
        }
112

113
        config.params.modtools = miscStore.modtools // MT
×
114

115
        // JSON-encode these for to pass.
116
        body = JSON.stringify(config.params)
×
117
      } else if (!config?.formPost) {
248!
118
        // Parameters will be passed in config.data.
119
        if (!config.data) {
248!
120
          config.data = {}
×
121
        }
122

123
        config.data.modtools = miscStore.modtools // MT
248✔
124
        body = JSON.stringify(config.data)
248✔
125
      }
126

127
      await miscStore.waitForOnline()
327✔
128
      miscStore.api(1)
327✔
129
      ;[status, data] = await ourFetch(this.config.public.APIv1 + path, {
327✔
130
        ...config,
131
        body,
132
        method,
133
        headers,
134
      })
135

136
      if (
327✔
137
        data.jwt &&
382✔
138
        data.jwt !== authStore.auth.jwt &&
139
        data.persistent &&
140
        path.substring(0, 5) !== '/user'
141
      ) {
142
        // Stop MT add user from switching identity
143
        // We've been given a new JWT.  Use it in future.  This can happen after user merge or periodically when
144
        // we renew the JWT.
145
        authStore.setAuth(data.jwt, data.persistent)
11✔
146
      }
147
    } catch (e) {
UNCOV
148
      if (e.message.match(/.*aborted.*/i)) {
×
149
        // We've seen requests get aborted immediately after beforeunload().  Makes sense to abort the requests
150
        // when you're leaving a page.  No point in rippling those errors up to result in Sentry errors.
151
        // Swallow these by returning a problem that never resolves.  Possible memory leak but it's a rare case.
152
        console.log('Aborted - ignore')
×
153
        return new Promise(function (resolve) {})
×
154
      }
155
    } finally {
156
      useMiscStore().api(-1)
327✔
157
    }
158

159
    // HTTP errors are real errors.
160
    //
161
    // We've sometimes seen 200 response codes with no returned data (I saw this myself on a train with flaky
162
    // signal).  So that's an error if it happens.
163
    //
164
    // data.ret holds the server error.
165
    // - 1 means not logged in, and that's ok.
166
    // - POSTs to session can return errors we want to handle.
167
    // - 999 can happen if people double-click, and we should just quietly drop it because the first click will
168
    //   probably do the right thing.
169
    // - otherwise throw an exception.
170
    if (
327✔
171
      status !== 200 ||
1,038✔
172
      !data ||
173
      (data.ret !== 0 &&
174
        !(data.ret === 1 && data.status === 'Not logged in') &&
59✔
175
        !(path === '/session' && method === 'POST') &&
11!
176
        data.ret !== 999)
177
    ) {
178
      const retstr = data && data.ret ? data.ret : 'Unknown'
3!
179
      const statusstr = data && data.status ? data.status : 'Unknown'
3!
180

181
      if (retstr === 111) {
3!
182
        // Down for maintenance
183
        console.log('Down for maintenance')
×
184
        throw new MaintenanceError(data.ret, 'Maintenance error')
×
185
      } else {
186
        // Whether or not we log this error to Sentry depends.  Most errors are worth logging, because they're unexpected.
187
        // But some API calls are expected to fail, and throw an exception which is then handled in the code.  We don't
188
        // want to log those, otherwise we will spend time investigating them in Sentry.  So we have a parameter which
189
        // indicates whether we want to log this to Sentry - which can be a boolean or a function for more complex
190
        // decisions.
191
        const log = typeof logError === 'function' ? logError(data) : logError
3!
192
        console.log('Log it?', log)
3✔
193

194
        if (
3!
195
          log &&
3!
196
          (status !== null || retstr !== 'Unknown' || statusstr !== 'Unknown')
197
        ) {
198
          Sentry.captureMessage(
×
199
            'API request failed ' +
200
              path +
201
              ' returned HTTP ' +
202
              status +
203
              ' ret ' +
204
              retstr +
205
              ' status ' +
206
              statusstr
207
          )
208
        }
209

210
        const message = [
3✔
211
          'API Error',
212
          method,
213
          path,
214
          '->',
215
          `ret: ${retstr} status: ${statusstr}`,
216
        ].join(' ')
217

218
        throw new APIError(
3✔
219
          {
220
            request: {
221
              path,
222
              method,
223
              headers: config.headers,
224
              params: config.params,
225
              data: config.data,
226
            },
227
            response: {
228
              status,
229
              data,
230
            },
231
          },
232
          message
233
        )
234
      }
235
    }
236

237
    return data
324✔
238
  }
239

240
  $get(path, params = {}, logError = true) {
158✔
241
    return this.$request('GET', path, { params }, logError)
79✔
242
  }
243

244
  $post(path, data, logError = true) {
230✔
245
    return this.$request(
230✔
246
      'POST',
247
      path,
248
      {
249
        data,
250
      },
251
      logError
252
    )
253
  }
254

255
  $postv2(path, data, logError = true) {
×
256
    const authStore = useAuthStore()
×
257

258
    return this.$requestv2(
×
259
      'POST',
260
      path,
261
      {
262
        headers: {
263
          'Content-Type': 'application/json',
264
          Authorization: 'Iznik ' + JSON.stringify(authStore.auth?.persistent),
×
265
        },
266
        data,
267
      },
268
      logError
269
    )
270
  }
271

272
  $postForm(path, data, logError = true) {
×
273
    // Don't set Content-Type - see https://stackoverflow.com/questions/39280438/fetch-missing-boundary-in-multipart-form-data-post
274
    return this.$request(
×
275
      'POST',
276
      path,
277
      {
278
        formPost: true,
279
      },
280
      logError,
281
      data
282
    )
283
  }
284

285
  $postOverride(overrideMethod, path, data, logError = true) {
18✔
286
    return this.$request(
18✔
287
      'POST',
288
      path,
289
      {
290
        data,
291
        headers: {
292
          'X-HTTP-Method-Override': overrideMethod,
293
        },
294
      },
295
      logError
296
    )
297
  }
298

299
  $put(path, data, logError = true) {
6✔
300
    return this.$postOverride('PUT', path, data, logError)
6✔
301
  }
302

303
  $patch(path, data, logError = true) {
12✔
304
    return this.$postOverride('PATCH', path, data, logError)
12✔
305
  }
306

307
  $del(path, data, config = {}, logError = true) {
×
UNCOV
308
    return this.$postOverride('DELETE', path, data, logError)
×
309
  }
310

311
  async $requestv2(method, path, config, logError = true, body = null) {
846✔
312
    // timer++
313
    // const timerLabel = path + ' api-' + timer
314

315
    // console.log('Start ', timerLabel)
316
    // console.time(timerLabel)
317

318
    let status = null
423✔
319
    let data = null
423✔
320
    const headers = config.headers ? config.headers : {}
423!
321

322
    try {
423✔
323
      const authStore = useAuthStore()
423✔
324
      const miscStore = useMiscStore()
423✔
325

326
      if (authStore?.auth?.jwt) {
423!
327
        // Use the JWT to authenticate the request if possible.
328
        headers.Authorization = JSON.stringify(authStore.auth.jwt)
411✔
329
      }
330

331
      if (authStore?.auth?.persistent) {
423!
332
        // The JWT is quick but short-lived; use the persistent token as a fallback.
333
        headers.Authorization2 = JSON.stringify(authStore.auth.persistent)
411✔
334
      }
335

336
      const loggedInAs = authStore.user?.id
423✔
337

338
      if (loggedInAs) {
423✔
339
        // Add the user ID as a query parameter to the path, checking whether there already are any
340
        // query parameters.
341
        path += (path.includes('?') ? '&' : '?') + 'loggedInAs=' + loggedInAs
402✔
342
      }
343

344
      // Add requestId to the path, checking whether there already are any query parameters.
345
      path += (path.includes('?') ? '&' : '?') + 'requestid=' + requestId++
423✔
346

347
      headers['Cache-Control'] =
423✔
348
        'max-age=0, must-revalidate, no-cache, no-store, private'
349

350
      if (method === 'GET' && config?.params) {
423!
351
        // Remove falsey values from the params.
352
        config.params = Object.fromEntries(
423✔
353
          Object.entries(config.params).filter(([_, v]) => v)
354
        )
355

356
        // URL encode the parameters if any
357
        const urlParams = new URLSearchParams(config.params).toString()
423✔
358

359
        if (urlParams.length) {
423✔
360
          path += '&' + urlParams
124✔
361
        }
362
      } else if (method !== 'POST') {
×
363
        // Any parameters are passed in config.params.
364
        if (!config?.params) {
×
365
          config.params = {}
×
366
        }
367

368
        config.params.modtools = miscStore.modtools // MT
×
369

370
        // JSON-encode these for to pass.
371
        body = JSON.stringify(config.params)
×
372
      } else if (!config?.formPost) {
×
373
        // Parameters will be passed in config.data.
374
        if (!config.data) {
×
375
          config.data = {}
×
376
        }
377

378
        if (!config.params) {
×
379
          config.params = {}
×
380
        }
381

382
        console.log(
×
383
          'Seet MT in config.params',
384
          config,
385
          config.params,
386
          miscStore.modtools
387
        )
388

389
        config.params.modtools = miscStore.modtools
×
390
        body = JSON.stringify(config.data)
×
391
      }
392

393
      await miscStore.waitForOnline()
423✔
394
      miscStore.api(1)
423✔
395
      ;[status, data] = await ourFetch(this.config.public.APIv2 + path, {
423✔
396
        ...config,
397
        body,
398
        method,
399
        headers,
400
      })
401

402
      if (status === 401) {
423!
403
        // Not authorised - our JWT and/or persistent token must be wrong.  Clear them.  This may force a login, or
404
        // not, depending on whether the page requires it.
405
        console.log('Not authorised - force logged out')
×
406
        authStore.setAuth(null, null)
×
407
        authStore.setUser(null)
×
408

409
        // For specific paths, we want to silently allow 401 errors and swallow them.
410
        // This can happen if a login token is invalid, and we don't want to show errors to the user.
411
        if (path.startsWith('/chat?includeClosed=true')) {
×
412
          console.log('Silently handling 401 for includeClosed chat request')
×
413
          return new Promise(function (resolve) {})
×
414
        }
415
      }
416
    } catch (e) {
UNCOV
417
      console.log('Fetch error', path, e?.message)
×
UNCOV
418
      if (e?.response?.status) {
×
419
        status = e.response.status
×
420
      }
421

UNCOV
422
      if (e.message.match(/.*aborted.*/i)) {
×
423
        // We've seen requests get aborted immediately after beforeunload().  Makes sense to abort the requests
424
        // when you're leaving a page.  No point in rippling those errors up to result in Sentry errors.
425
        // Swallow these by returning a problem that never resolves.  Possible memory leak but it's a rare case.
426
        console.log('Aborted - ignore')
×
427
        return new Promise(function (resolve) {})
×
428
      }
429
    } finally {
430
      useMiscStore().api(-1)
423✔
431
    }
432

433
    // HTTP errors are real errors.
434
    //
435
    // data.ret holds the server error.
436
    // - 1 means not logged in, and that's ok.
437
    // - POSTs to session can return errors we want to handle.
438
    // - 999 can happen if people double-click, and we should just quietly drop it because the first click will
439
    //   probably do the right thing.
440
    // - otherwise throw an exception.
441
    if (status !== 200) {
423!
UNCOV
442
      const statusstr = status?.toString()
×
443

444
      // For specific paths, we want to silently allow 401 errors and swallow them.
445
      // This can happen if a login token is invalid, and we don't want to show errors to the user.
UNCOV
446
      if (status === 401 && path.startsWith('/chat?includeClosed=true')) {
×
447
        console.log('Silently handling 401 for includeClosed chat request')
×
448
        return new Promise(function (resolve) {})
×
449
      }
450

451
      // Whether or not we log this error to Sentry depends.  Most errors are worth logging, because they're unexpected.
452
      // But some API calls are expected to fail, and throw an exception which is then handled in the code.  We don't
453
      // want to log those, otherwise we will spend time investigating them in Sentry.  So we have a parameter which
454
      // indicates whether we want to log this to Sentry - which can be a boolean or a function for more complex
455
      // decisions.
UNCOV
456
      const log = typeof logError === 'function' ? logError(data) : logError
×
457

UNCOV
458
      if (log && (status !== null || statusstr !== 'Unknown')) {
×
UNCOV
459
        Sentry.captureMessage(
×
460
          'API2 request failed ' +
461
            path +
462
            ' returned HTTP ' +
463
            status +
464
            ' status ' +
465
            statusstr +
466
            ' data length ' +
467
            (data ? data.length : 0)
×
468
        )
469
      }
470

UNCOV
471
      const message = [
×
472
        'API Error',
473
        method,
474
        path,
475
        '->',
476
        `status: ${statusstr}`,
477
      ].join(' ')
478

UNCOV
479
      throw new APIError(
×
480
        {
481
          request: {
482
            path,
483
            method,
484
            headers: config.headers,
485
            params: config.params,
486
            data: config.data,
487
          },
488
          response: {
489
            status,
490
            data,
491
          },
492
        },
493
        message
494
      )
495
    }
496

497
    // console.timeEnd(timerLabel)
498

499
    return data
423✔
500
  }
501

502
  $getv2(path, params = {}, logError = true) {
846✔
503
    return this.$requestv2('GET', path, { params }, logError)
423✔
504
  }
505
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc