Skip to content

Commit ac1cb53

Browse files
committed
Fix 403 errors
1 parent 6a2c5a7 commit ac1cb53

9 files changed

Lines changed: 633 additions & 54 deletions

File tree

src/components/Search/SearchBar/client/__tests__/index.spec.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ import { __resetHeaderSearchForTests } from '@components/scripts/store/search'
1010

1111
type SearchBarModule = WebComponentModule<SearchBarElementInstance>
1212

13-
type ActionResult<TData> = { data?: TData; error?: { message?: string } }
13+
type ActionResult<TData> = {
14+
data?: TData
15+
error?: { code?: string; message?: string; status?: number }
16+
}
1417

1518
const searchQueryMock =
1619
vi.fn<(_input: { q: string; limit?: number }) => Promise<ActionResult<{ hits: SearchHit[] }>>>()
20+
const handleScriptErrorMock = vi.hoisted(() => vi.fn())
1721

1822
vi.mock('astro:actions', () => ({
1923
actions: {
@@ -23,6 +27,10 @@ vi.mock('astro:actions', () => ({
2327
},
2428
}))
2529

30+
vi.mock('@components/scripts/errors/handler', () => ({
31+
handleScriptError: handleScriptErrorMock,
32+
}))
33+
2634
const flushMicrotasks = async () => {
2735
await Promise.resolve()
2836
await Promise.resolve()
@@ -53,6 +61,7 @@ describe('SearchBar web component', () => {
5361
beforeEach(async () => {
5462
container = await AstroContainer.create()
5563
searchQueryMock.mockReset()
64+
handleScriptErrorMock.mockReset()
5665

5766
__resetHeaderSearchForTests()
5867

@@ -218,6 +227,56 @@ describe('SearchBar web component', () => {
218227
vi.useRealTimers()
219228
})
220229

230+
it('silently ignores forbidden action results', async () => {
231+
vi.useFakeTimers()
232+
233+
await runComponentRender(async ({ element, window }) => {
234+
searchQueryMock.mockResolvedValue({
235+
error: {
236+
code: 'FORBIDDEN',
237+
message: 'HTTP Client Error with status code: 403',
238+
status: 403,
239+
},
240+
})
241+
242+
const input = element.querySelector('[data-search-input]') as HTMLInputElement
243+
const resultsContainer = element.querySelector('[data-search-results]') as HTMLElement
244+
245+
input.value = 'blocked'
246+
input.dispatchEvent(new window.Event('input', { bubbles: true }))
247+
248+
await vi.advanceTimersByTimeAsync(260)
249+
await flushMicrotasks()
250+
251+
expect(handleScriptErrorMock).not.toHaveBeenCalled()
252+
expect(resultsContainer.classList.contains('hidden')).toBe(true)
253+
})
254+
255+
vi.useRealTimers()
256+
})
257+
258+
it('silently ignores forbidden thrown action errors', async () => {
259+
vi.useFakeTimers()
260+
261+
await runComponentRender(async ({ element, window }) => {
262+
searchQueryMock.mockRejectedValue(new Error('HTTP Client Error with status code: 403'))
263+
264+
const input = element.querySelector('[data-search-input]') as HTMLInputElement
265+
const resultsContainer = element.querySelector('[data-search-results]') as HTMLElement
266+
267+
input.value = 'blocked'
268+
input.dispatchEvent(new window.Event('input', { bubbles: true }))
269+
270+
await vi.advanceTimersByTimeAsync(260)
271+
await flushMicrotasks()
272+
273+
expect(handleScriptErrorMock).not.toHaveBeenCalled()
274+
expect(resultsContainer.classList.contains('hidden')).toBe(true)
275+
})
276+
277+
vi.useRealTimers()
278+
})
279+
221280
it('toggles open and closes on Escape in header variant', async () => {
222281
await runHeaderComponentRender(async ({ element, window }) => {
223282
const toggleBtn = element.querySelector('[data-search-toggle]') as HTMLButtonElement

src/components/Search/SearchBar/client/index.ts

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { render } from 'lit/html.js'
33
import { actions } from 'astro:actions'
44
import { defineCustomElement } from '@components/scripts/utils'
55
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
6+
import {
7+
isForbiddenClientActionError,
8+
normalizeClientActionError,
9+
} from '@components/scripts/errors/actionClient'
610
import { handleScriptError } from '@components/scripts/errors/handler'
711
import { addScriptBreadcrumb } from '@components/scripts/errors'
812
import {
@@ -670,38 +674,57 @@ export class SearchBarElement extends LitElement {
670674
addScriptBreadcrumb(context)
671675

672676
const requestId = ++this.latestRequestId
673-
const { data, error } = await actions.search.query({
674-
q: query,
675-
limit: HEADER_SEARCH_RESULT_LIMIT,
676-
})
677+
try {
678+
const { data, error } = await actions.search.query({
679+
q: query,
680+
limit: HEADER_SEARCH_RESULT_LIMIT,
681+
})
682+
const actionError = normalizeClientActionError(error)
677683

678-
// @TODO: Improve this error handling to be more user friendly. Should look at the types of errors that could occur, and give the user an idea of what to do.
679-
if (error) {
680-
handleScriptError(error, context)
681-
this.clearResults()
682-
this.hideResults()
683-
return
684-
}
684+
// @TODO: Improve this error handling to be more user friendly. Should look at the types of errors that could occur, and give the user an idea of what to do.
685+
if (error) {
686+
if (isForbiddenClientActionError(actionError)) {
687+
this.clearResults()
688+
this.hideResults()
689+
return
690+
}
685691

686-
if (!data) {
687-
this.clearResults()
688-
this.hideResults()
689-
return
690-
}
692+
handleScriptError(error, context)
693+
this.clearResults()
694+
this.hideResults()
695+
return
696+
}
691697

692-
if (requestId !== this.latestRequestId) {
693-
return
694-
}
698+
if (!data) {
699+
this.clearResults()
700+
this.hideResults()
701+
return
702+
}
695703

696-
const hits = (data.hits ?? []) as SearchHit[]
697-
if (hits.length === 0) {
704+
if (requestId !== this.latestRequestId) {
705+
return
706+
}
707+
708+
const hits = (data.hits ?? []) as SearchHit[]
709+
if (hits.length === 0) {
710+
this.clearResults()
711+
this.hideResults()
712+
return
713+
}
714+
715+
this.renderResults(query, hits)
716+
this.showResults()
717+
} catch (error) {
718+
if (isForbiddenClientActionError(normalizeClientActionError(error))) {
719+
this.clearResults()
720+
this.hideResults()
721+
return
722+
}
723+
724+
handleScriptError(error, context)
698725
this.clearResults()
699726
this.hideResults()
700-
return
701727
}
702-
703-
this.renderResults(query, hits)
704-
this.showResults()
705728
}
706729
}
707730

src/components/Search/SearchResults/client/__tests__/index.spec.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import { executeRender } from '@test/unit/helpers/litRuntime'
77

88
type SearchResultsModule = WebComponentModule<SearchResultsElementInstance>
99

10-
type ActionResult<TData> = { data?: TData; error?: { message?: string } }
10+
type ActionResult<TData> = {
11+
data?: TData
12+
error?: { code?: string; message?: string; status?: number }
13+
}
1114

1215
const searchQueryMock =
1316
vi.fn<
@@ -16,6 +19,7 @@ const searchQueryMock =
1619
limit?: number
1720
}) => Promise<ActionResult<{ hits: { title: string; url: string; snippet?: string }[] }>>
1821
>()
22+
const handleScriptErrorMock = vi.hoisted(() => vi.fn())
1923

2024
vi.mock('astro:actions', () => ({
2125
actions: {
@@ -25,6 +29,10 @@ vi.mock('astro:actions', () => ({
2529
},
2630
}))
2731

32+
vi.mock('@components/scripts/errors/handler', () => ({
33+
handleScriptError: handleScriptErrorMock,
34+
}))
35+
2836
const flushMicrotasks = async () => {
2937
await Promise.resolve()
3038
await Promise.resolve()
@@ -36,6 +44,7 @@ describe('SearchResults web component', () => {
3644
beforeEach(async () => {
3745
container = await AstroContainer.create()
3846
searchQueryMock.mockReset()
47+
handleScriptErrorMock.mockReset()
3948
})
4049

4150
const runComponentRender = async (
@@ -197,6 +206,40 @@ describe('SearchResults web component', () => {
197206
})
198207
})
199208

209+
it('silently ignores forbidden action results', async () => {
210+
searchQueryMock.mockResolvedValue({
211+
error: {
212+
code: 'FORBIDDEN',
213+
message: 'HTTP Client Error with status code: 403',
214+
status: 403,
215+
},
216+
})
217+
218+
await runComponentRender({ query: 'blocked' }, async ({ element }) => {
219+
await flushMicrotasks()
220+
221+
expect(handleScriptErrorMock).not.toHaveBeenCalled()
222+
expect(element.querySelector('[data-search-results] li')).toBeNull()
223+
224+
const error = element.querySelector('[data-search-error]')
225+
expect(error?.classList.contains('hidden')).toBe(true)
226+
})
227+
})
228+
229+
it('silently ignores forbidden thrown action errors', async () => {
230+
searchQueryMock.mockRejectedValue(new Error('HTTP Client Error with status code: 403'))
231+
232+
await runComponentRender({ query: 'blocked' }, async ({ element }) => {
233+
await flushMicrotasks()
234+
235+
expect(handleScriptErrorMock).not.toHaveBeenCalled()
236+
expect(element.querySelector('[data-search-results] li')).toBeNull()
237+
238+
const error = element.querySelector('[data-search-error]')
239+
expect(error?.classList.contains('hidden')).toBe(true)
240+
})
241+
})
242+
200243
it('supports a custom limit without rendering the built-in empty state', async () => {
201244
searchQueryMock.mockResolvedValue({
202245
data: {

src/components/Search/SearchResults/client/index.ts

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { LitElement } from 'lit'
22
import { actions } from 'astro:actions'
33
import { defineCustomElement } from '@components/scripts/utils'
44
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
5+
import {
6+
isForbiddenClientActionError,
7+
normalizeClientActionError,
8+
} from '@components/scripts/errors/actionClient'
59
import { handleScriptError } from '@components/scripts/errors/handler'
610
import { addScriptBreadcrumb } from '@components/scripts/errors'
711
import { addButtonEventListeners } from '@components/scripts/elementListeners'
@@ -578,39 +582,62 @@ export class SearchResultsElement extends LitElement {
578582
addScriptBreadcrumb(context)
579583
const requestId = ++this.latestRequestId
580584

581-
const { data, error } = await actions.search.query({ q: query, limit: this.limit })
585+
try {
586+
const { data, error } = await actions.search.query({ q: query, limit: this.limit })
587+
const actionError = normalizeClientActionError(error)
582588

583-
// @TODO: Improve this error handling to be more user friendly. Should look at the types of errors that could occur, and give the user an idea of what to do.
584-
if (error) {
585-
const message = error instanceof Error ? error.message : 'Search failed.'
586-
handleScriptError(error, context)
587-
this.renderResults([])
588-
this.clearMeta()
589-
this.showError(message)
590-
return
591-
}
589+
// @TODO: Improve this error handling to be more user friendly. Should look at the types of errors that could occur, and give the user an idea of what to do.
590+
if (error) {
591+
const message = actionError?.message ?? (error instanceof Error ? error.message : 'Search failed.')
592592

593-
if (!data) {
594-
this.renderResults([])
595-
this.clearMeta()
596-
return
597-
}
593+
if (isForbiddenClientActionError(actionError)) {
594+
this.renderResults([])
595+
this.clearMeta()
596+
return
597+
}
598598

599-
if (requestId !== this.latestRequestId) {
600-
return
601-
}
599+
handleScriptError(error, context)
600+
this.renderResults([])
601+
this.clearMeta()
602+
this.showError(message)
603+
return
604+
}
605+
606+
if (!data) {
607+
this.renderResults([])
608+
this.clearMeta()
609+
return
610+
}
602611

603-
const hits = (data.hits ?? []) as SearchHit[]
604-
this.renderResults(hits)
605-
if (hits.length === 0 && this.shouldSuppressMetaFeedback()) {
612+
if (requestId !== this.latestRequestId) {
613+
return
614+
}
615+
616+
const hits = (data.hits ?? []) as SearchHit[]
617+
this.renderResults(hits)
618+
if (hits.length === 0 && this.shouldSuppressMetaFeedback()) {
619+
this.clearMeta()
620+
return
621+
}
622+
623+
this.setMeta(
624+
this.getResultsMetaMessage(query, hits),
625+
hits.length > 0 || !this.shouldSuppressMetaFeedback()
626+
)
627+
} catch (error) {
628+
const actionError = normalizeClientActionError(error)
629+
630+
if (isForbiddenClientActionError(actionError)) {
631+
this.renderResults([])
632+
this.clearMeta()
633+
return
634+
}
635+
636+
handleScriptError(error, context)
637+
this.renderResults([])
606638
this.clearMeta()
607-
return
639+
this.showError(actionError?.message ?? (error instanceof Error ? error.message : 'Search failed.'))
608640
}
609-
610-
this.setMeta(
611-
this.getResultsMetaMessage(query, hits),
612-
hits.length > 0 || !this.shouldSuppressMetaFeedback()
613-
)
614641
}
615642
}
616643

0 commit comments

Comments
 (0)