Serve 503 when PocketBase is unavailable across all content pages

This commit is contained in:
tleininger committed 2026-09-24 13:00:14 +02:00
1 parent 0f7e968c16
commit e242eaf79a
25 files changed
+492 -253

No files matched your search

+38 -10
View File
@@ -38,14 +38,11 @@ public sealed class PocketBaseClient(HttpClient httpClient)
/// <returns>
/// The public pages, or an empty list if there are none.
/// </returns>
/// <exception cref="HttpRequestException">
/// PocketBase could not be reached or returned a non-success status code.
/// </exception>
/// <exception cref="JsonException">
/// The response could not be deserialized.
/// <exception cref="PocketBaseUnavailableException">
/// PocketBase could not be reached, timed out, or returned an unreadable response.
/// </exception>
/// <exception cref="OperationCanceledException">
/// <paramref name="token"/> was canceled or the request timed out.
/// <paramref name="token"/> was canceled by the caller.
/// </exception>
public Task<IReadOnlyList<Page>> GetPagesAsync(CancellationToken token = default)
=> GetRecordsAsync<Page>("pages", "order", token);
@@ -73,15 +70,22 @@ public sealed class PocketBaseClient(HttpClient httpClient)
=> GetRecordsAsync<Event>("events", "start", token);
/// <summary>
/// Gets all public FAQ entries, ordered by <see cref="Faq.Topic"/>.
/// Gets all public FAQ entries, ordered by <see cref="Faq.Topic"/>, then by
/// <see cref="Faq.Question"/>.
/// </summary>
/// <remarks>
/// The second sort key makes the order total: sorting by topic alone leaves
/// entries within a topic in an undefined order, so the same content could
/// render differently between requests. Adding the question as a tie-breaker
/// keeps the FAQ list and any teaser drawn from it stable.
/// </remarks>
/// <inheritdoc cref="GetPagesAsync" path="/param"/>
/// <returns>
/// The public FAQ entries, or an empty list if there are none.
/// </returns>
/// <inheritdoc cref="GetPagesAsync" path="/exception"/>
public Task<IReadOnlyList<Faq>> GetFaqsAsync(CancellationToken token = default)
=> GetRecordsAsync<Faq>("faqs", "topic", token);
=> GetRecordsAsync<Faq>("faqs", "topic,question", token);
/// <summary>
/// Gets all public records of a collection in a single request.
@@ -101,6 +105,12 @@ public sealed class PocketBaseClient(HttpClient httpClient)
/// <returns>
/// The public records, or an empty list if there are none.
/// </returns>
/// <exception cref="PocketBaseUnavailableException">
/// PocketBase could not be reached, timed out, or returned an unreadable response.
/// </exception>
/// <exception cref="OperationCanceledException">
/// <paramref name="token"/> was canceled by the caller.
/// </exception>
private async Task<IReadOnlyList<T>> GetRecordsAsync<T>(
string collection,
string sort,
@@ -112,7 +122,25 @@ public sealed class PocketBaseClient(HttpClient httpClient)
+ $"?perPage=500&filter={Uri.EscapeDataString("public=true")}"
+ $"&sort={Uri.EscapeDataString(sort)}";
var result = await httpClient.GetFromJsonAsync<RecordList<T>>(url, token);
return result?.Items ?? [];
try
{
var result = await httpClient.GetFromJsonAsync<RecordList<T>>(url, token);
return result?.Items ?? [];
}
// These three mean PocketBase itself failed: it was unreachable
// (HttpRequestException), the request to it timed out (a TaskCanceledException
// whose inner exception is a TimeoutException, which is how HttpClient surfaces
// its own timeout), or it answered with something we could not read
// (JsonException). A plain TaskCanceledException with no inner TimeoutException
// is the caller's own cancellation via token and is deliberately not caught --
// it propagates as OperationCanceledException so an aborted request stays an
// abort, not a fault.
catch (Exception exception) when (
exception is HttpRequestException
or JsonException
or TaskCanceledException { InnerException: TimeoutException })
{
throw new PocketBaseUnavailableException($"Could not read collection '{collection}'.", exception);
}
}
}