Serve 503 when PocketBase is unavailable across all content pages
This commit is contained in:
1 parent
0f7e968c16
commit
e242eaf79a
25 files changed
+492
-253
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace Elternbeirat.PocketBase;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when PocketBase could not be reached, timed out, or returned a
|
||||
/// response that could not be read.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the one failure the content pages need to distinguish: the site is
|
||||
/// up but its data store is not, which is an HTTP 503, not a 404. Which
|
||||
/// low-level exceptions mean that (a transport error, a timeout, unreadable
|
||||
/// JSON) is <see cref="PocketBaseClient"/>'s concern; callers catch only this
|
||||
/// type and need not know how the client talks to PocketBase.
|
||||
/// </remarks>
|
||||
public sealed class PocketBaseUnavailableException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance with a default message.
|
||||
/// </summary>
|
||||
public PocketBaseUnavailableException()
|
||||
: base("PocketBase is unavailable.")
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance with the given message.
|
||||
/// </summary>
|
||||
/// <param name="message">
|
||||
/// The message that describes the error.
|
||||
/// </param>
|
||||
public PocketBaseUnavailableException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance with the given message and the underlying
|
||||
/// failure that caused it.
|
||||
/// </summary>
|
||||
/// <param name="message">
|
||||
/// The message that describes the error.
|
||||
/// </param>
|
||||
/// <param name="innerException">
|
||||
/// The transport, timeout, or deserialization exception that was caught.
|
||||
/// </param>
|
||||
public PocketBaseUnavailableException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user