diff options
| author | Craig Jennings <c@cjennings.net> | 2026-04-04 16:32:16 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-04-04 16:32:16 -0500 |
| commit | 73c81a00a10766900318d86640249d1b54c6b351 (patch) | |
| tree | 793f9c858060591c34813af05e84c7a6a5442153 /wttrin.el | |
| parent | a77a7b86f45ae96ff1802ea6f8b87dafd46b17b0 (diff) | |
| download | emacs-wttrin-73c81a00a10766900318d86640249d1b54c6b351.tar.gz emacs-wttrin-73c81a00a10766900318d86640249d1b54c6b351.zip | |
feat: specific error messages for fetch failures
Add HTTP status code checking (wttrin--extract-http-status) and pass
error descriptions through the callback chain so users see "Location
not found (HTTP 404)" or "Network error — check your connection"
instead of the generic "Perhaps the location was misspelled?" for
every failure.
Also fix pre-existing bug where the condition-case error handler in
extract-response-body killed an unrelated buffer after unwind-protect
already cleaned up.
330 tests (was 307), all passing.
Diffstat (limited to 'wttrin.el')
| -rw-r--r-- | wttrin.el | 94 |
1 files changed, 65 insertions, 29 deletions
@@ -249,43 +249,75 @@ Returns \"just now\" for <60s, \"X minutes ago\", \"X hours ago\", or \"X days a (wttrin-additional-url-params) "A")) +(defun wttrin--extract-http-status () + "Return the HTTP status code from the current buffer, or nil. +Reads the status line without moving point." + (save-excursion + (goto-char (point-min)) + (when (re-search-forward "^HTTP/[0-9.]+ \\([0-9]+\\)" nil t) + (string-to-number (match-string 1))))) + (defun wttrin--extract-response-body () "Extract and decode HTTP response body from current buffer. Skips headers and returns UTF-8 decoded body. -Returns nil on error. Kills buffer when done." +Returns nil for non-2xx status codes or on error. Kills buffer when done." (condition-case err (unwind-protect - (progn - (goto-char (point-min)) - ;; Skip past HTTP headers — blank line separates headers from body - (re-search-forward "\r?\n\r?\n" nil t) - (let ((body (decode-coding-string - (buffer-substring-no-properties (point) (point-max)) - 'utf-8))) - (wttrin--debug-log "wttrin--extract-response-body: Successfully fetched %d bytes" - (length body)) - body)) + (let ((status (wttrin--extract-http-status))) + (if (and status (>= status 300)) + (progn + (wttrin--debug-log "wttrin--extract-response-body: HTTP %d" status) + nil) + (goto-char (point-min)) + ;; Skip past HTTP headers — blank line separates headers from body + (re-search-forward "\r?\n\r?\n" nil t) + (let ((body (decode-coding-string + (buffer-substring-no-properties (point) (point-max)) + 'utf-8))) + (wttrin--debug-log "wttrin--extract-response-body: Successfully fetched %d bytes" + (length body)) + body))) + ;; unwind-protect handles buffer cleanup for all paths (ignore-errors (kill-buffer (current-buffer)))) (error (wttrin--debug-log "wttrin--extract-response-body: Error - %s" (error-message-string err)) - (ignore-errors (kill-buffer (current-buffer))) nil))) (defun wttrin--handle-fetch-callback (status callback) "Handle `url-retrieve' callback STATUS and invoke CALLBACK with result. -Extracts response body or handles errors, then calls CALLBACK with data or nil." +Calls CALLBACK with (DATA &optional ERROR-MSG). DATA is the response +body string on success, nil on failure. ERROR-MSG is a human-readable +description of what went wrong, or nil on success." (wttrin--debug-log "wttrin--handle-fetch-callback: Invoked with status = %S" status) - (let ((data nil)) - (if (plist-get status :error) - (wttrin--debug-log "wttrin--handle-fetch-callback: Network error - %s" - (cdr (plist-get status :error))) - (setq data (wttrin--extract-response-body))) + (let ((data nil) + (error-msg nil)) + (cond + ;; Network-level failure (DNS, connection refused, timeout) + ((plist-get status :error) + (wttrin--debug-log "wttrin--handle-fetch-callback: Network error - %s" + (cdr (plist-get status :error))) + (setq error-msg "Network error — check your connection") + (message "wttrin: %s" error-msg)) + ;; HTTP response received — extract body (returns nil for non-2xx) + (t + (let ((http-status (wttrin--extract-http-status))) + (setq data (wttrin--extract-response-body)) + (when (and (not data) http-status) + (setq error-msg + (cond + ((and (>= http-status 400) (< http-status 500)) + (format "Location not found (HTTP %d)" http-status)) + ((>= http-status 500) + (format "Weather service error (HTTP %d)" http-status)) + (t (format "Unexpected HTTP status %d" http-status)))) + (when error-msg + (message "wttrin: %s" error-msg)))))) (condition-case err (progn (wttrin--debug-log "wttrin--handle-fetch-callback: Calling user callback with %s" (if data (format "%d bytes" (length data)) "nil")) - (funcall callback data)) + (funcall callback data error-msg)) (error (wttrin--debug-log "wttrin--handle-fetch-callback: Error in user callback - %s" (error-message-string err)) @@ -394,13 +426,17 @@ Looks up the cache timestamp for LOCATION and formats a line like (age-str (wttrin--format-age age))) (format "Last updated: %s (%s)" (string-trim time-str) age-str))))) -(defun wttrin--display-weather (location-name raw-string) - "Display weather data RAW-STRING for LOCATION-NAME in weather buffer." +(defun wttrin--display-weather (location-name raw-string &optional error-msg) + "Display weather data RAW-STRING for LOCATION-NAME in weather buffer. +When ERROR-MSG is provided and data is invalid, show that instead of +the generic error message." (when wttrin-debug (wttrin--save-debug-data location-name raw-string)) (if (not (wttrin--validate-weather-data raw-string)) - (message "Cannot retrieve weather data. Perhaps the location was misspelled?") + (message "wttrin: %s" + (or error-msg + "Cannot retrieve weather data. Perhaps the location was misspelled?")) (let ((buffer (get-buffer-create (format "*wttr.in*")))) (switch-to-buffer buffer) @@ -434,10 +470,10 @@ Looks up the cache timestamp for LOCATION and formats a line like (setq buffer-read-only t) (wttrin--get-cached-or-fetch location-name - (lambda (raw-string) + (lambda (raw-string &optional error-msg) (when (buffer-live-p buffer) (with-current-buffer buffer - (wttrin--display-weather location-name raw-string))))))) + (wttrin--display-weather location-name raw-string error-msg))))))) (defun wttrin--make-cache-key (location) "Create cache key from LOCATION and current settings." @@ -447,7 +483,7 @@ Looks up the cache timestamp for LOCATION and formats a line like "Get cached weather for LOCATION or fetch if not cached. If cache has data and not force-refreshing, serves it immediately regardless of age. The background refresh timer keeps data fresh. -CALLBACK is called with the weather data string when ready, or nil on error." +CALLBACK is called with (DATA &optional ERROR-MSG)." (let* ((cache-key (wttrin--make-cache-key location)) (cached (gethash cache-key wttrin--cache)) (data (cdr cached))) @@ -456,7 +492,7 @@ CALLBACK is called with the weather data string when ready, or nil on error." (funcall callback data) (wttrin-fetch-raw-string location - (lambda (fresh-data) + (lambda (fresh-data &optional error-msg) (if fresh-data (progn (wttrin--cleanup-cache-if-needed) @@ -467,7 +503,7 @@ CALLBACK is called with the weather data string when ready, or nil on error." (progn (message "Failed to fetch new data, using cached version") (funcall callback data)) - (funcall callback nil)))))))) + (funcall callback nil error-msg)))))))) (defun wttrin--get-cache-entries-by-age () "Return list of (key . timestamp) pairs sorted oldest-first. @@ -578,7 +614,7 @@ On failure with no cache, shows error placeholder." (wttrin--debug-log "mode-line-fetch: URL = %s" url) (wttrin--fetch-url url - (lambda (data) + (lambda (data &optional _error-msg) (if data (let ((trimmed-data (string-trim data))) (wttrin--debug-log "mode-line-fetch: Received data = %S" trimmed-data) @@ -676,7 +712,7 @@ opens the weather buffer." (cache-key (wttrin--make-cache-key location))) (wttrin-fetch-raw-string location - (lambda (fresh-data) + (lambda (fresh-data &optional _error-msg) (when fresh-data (wttrin--cleanup-cache-if-needed) (puthash cache-key (cons (float-time) fresh-data) wttrin--cache))))))) |
