···2233import (
44 "context"
55+ "errors"
56 "fmt"
6778 "arabica/internal/metrics"
···1112 "github.com/bluesky-social/indigo/atproto/syntax"
1213 "github.com/rs/zerolog/log"
1314)
1515+1616+// ErrSessionExpired is returned when the OAuth session cannot be resumed,
1717+// indicating the user's authorization grant has expired and they need to log in again.
1818+var ErrSessionExpired = errors.New("oauth session expired")
14191520// Client wraps the atproto API client for making authenticated requests to a PDS
1621type Client struct {
···3035 // Resume the OAuth session - this returns a ClientSession that handles DPOP
3136 session, err := c.oauth.app.ResumeSession(ctx, did, sessionID)
3237 if err != nil {
3333- return nil, fmt.Errorf("failed to resume session: %w", err)
3838+ return nil, fmt.Errorf("%w: %w", ErrSessionExpired, err)
3439 }
35403641 // Get the authenticated API client from the session
+2-2
internal/handlers/brew.go
···567567568568 _, err := store.CreateBrew(r.Context(), req, 1) // User ID not used with atproto
569569 if err != nil {
570570- http.Error(w, "Failed to create brew", http.StatusInternalServerError)
571570 log.Error().Err(err).Msg("Failed to create brew")
571571+ handleStoreError(w, err, "Failed to create brew")
572572 return
573573 }
574574···655655656656 err := store.UpdateBrewByRKey(r.Context(), rkey, req)
657657 if err != nil {
658658- http.Error(w, "Failed to update brew", http.StatusInternalServerError)
659658 log.Error().Err(err).Str("rkey", rkey).Msg("Failed to update brew")
659659+ handleStoreError(w, err, "Failed to update brew")
660660 return
661661 }
662662
+8-8
internal/handlers/entities.go
···200200201201 bean, err := store.CreateBean(r.Context(), &req)
202202 if err != nil {
203203- http.Error(w, "Failed to create bean", http.StatusInternalServerError)
204203 log.Error().Err(err).Msg("Failed to create bean")
204204+ handleStoreError(w, err, "Failed to create bean")
205205 return
206206 }
207207···243243244244 roaster, err := store.CreateRoaster(r.Context(), &req)
245245 if err != nil {
246246- http.Error(w, "Failed to create roaster", http.StatusInternalServerError)
247246 log.Error().Err(err).Msg("Failed to create roaster")
247247+ handleStoreError(w, err, "Failed to create roaster")
248248 return
249249 }
250250···329329 }
330330331331 if err := store.UpdateBeanByRKey(r.Context(), rkey, &req); err != nil {
332332- http.Error(w, "Failed to update bean", http.StatusInternalServerError)
333332 log.Error().Err(err).Str("rkey", rkey).Msg("Failed to update bean")
333333+ handleStoreError(w, err, "Failed to update bean")
334334 return
335335 }
336336···392392 }
393393394394 if err := store.UpdateRoasterByRKey(r.Context(), rkey, &req); err != nil {
395395- http.Error(w, "Failed to update roaster", http.StatusInternalServerError)
396395 log.Error().Err(err).Str("rkey", rkey).Msg("Failed to update roaster")
396396+ handleStoreError(w, err, "Failed to update roaster")
397397 return
398398 }
399399···452452453453 grinder, err := store.CreateGrinder(r.Context(), &req)
454454 if err != nil {
455455- http.Error(w, "Failed to create grinder", http.StatusInternalServerError)
456455 log.Error().Err(err).Msg("Failed to create grinder")
456456+ handleStoreError(w, err, "Failed to create grinder")
457457 return
458458 }
459459···499499 }
500500501501 if err := store.UpdateGrinderByRKey(r.Context(), rkey, &req); err != nil {
502502- http.Error(w, "Failed to update grinder", http.StatusInternalServerError)
503502 log.Error().Err(err).Str("rkey", rkey).Msg("Failed to update grinder")
503503+ handleStoreError(w, err, "Failed to update grinder")
504504 return
505505 }
506506···558558559559 brewer, err := store.CreateBrewer(r.Context(), &req)
560560 if err != nil {
561561- http.Error(w, "Failed to create brewer", http.StatusInternalServerError)
562561 log.Error().Err(err).Msg("Failed to create brewer")
562562+ handleStoreError(w, err, "Failed to create brewer")
563563 return
564564 }
565565···604604 }
605605606606 if err := store.UpdateBrewerByRKey(r.Context(), rkey, &req); err != nil {
607607- http.Error(w, "Failed to update brewer", http.StatusInternalServerError)
608607 log.Error().Err(err).Str("rkey", rkey).Msg("Failed to update brewer")
608608+ handleStoreError(w, err, "Failed to update brewer")
609609 return
610610 }
611611
+13-1
internal/handlers/handlers.go
···33import (
44 "context"
55 "encoding/json"
66+ "errors"
67 "fmt"
78 "net/http"
89 "strings"
···226227 return
227228}
228229230230+// handleStoreError writes the appropriate HTTP error for a store operation failure.
231231+// If the error indicates an expired OAuth session, it returns 401 Unauthorized with
232232+// a user-friendly message. Otherwise it returns 500 with the fallbackMessage.
233233+func handleStoreError(w http.ResponseWriter, err error, fallbackMessage string) {
234234+ if errors.Is(err, atproto.ErrSessionExpired) {
235235+ http.Error(w, "Your session has expired. Please log in again.", http.StatusUnauthorized)
236236+ return
237237+ }
238238+ http.Error(w, fallbackMessage, http.StatusInternalServerError)
239239+}
240240+229241// deleteEntity validates the rkey, calls the delete function, and returns 200.
230242func (h *Handler) deleteEntity(w http.ResponseWriter, r *http.Request, deleteFn func(context.Context, string) error, entityName string) {
231243 rkey := validateRKey(w, r.PathValue("id"))
···233245 return
234246 }
235247 if err := deleteFn(r.Context(), rkey); err != nil {
236236- http.Error(w, "Failed to delete "+entityName, http.StatusInternalServerError)
237248 log.Error().Err(err).Str("rkey", rkey).Msg("Failed to delete " + entityName)
249249+ handleStoreError(w, err, "Failed to delete "+entityName)
238250 return
239251 }
240252 w.WriteHeader(http.StatusOK)