fix(openweather): invoke fall-through, request timeouts, and credential validator status-before-json - #3442
Conversation
…and missing-credential guards (#3456) * fix(google): return after invalid hl/gl error yields In `google_image_search.py._invoke()`, the language (`hl`) and country (`gl`) guards yield a friendly validation message but do not short-circuit afterwards. The HTTP request to SerpAPI still runs with the invalid values, and the user eventually sees a SerpAPI error body (`{"search_metadata": {"status": "error"}, ...}`) instead of the validation text. Add `return` after each guard so the generator exits cleanly with the user-visible message. Same root cause as the openweather plugin fix that landed in PR #3442. Addresses part of issue #3455 (Bug 2). * fix(google): add 10s timeout to both requests.get calls Both `GoogleSearch._invoke` and `GoogleImageSearch._invoke` use `requests.get` against SerpAPI without a `timeout` kwarg. When the remote host accepts the TCP connection but stalls (or is slowed by the network), the worker blocks until the framework-level `MAX_REQUEST_TIMEOUT=120` ceiling instead of failing with a clear "timed out" error. Add `timeout=10` to both call sites. 10s is well above the normal p99 for SerpAPI and well below the outer 120s ceiling, so a real slowdown is signalled within ~12s instead of being masked by the framework ceiling. Same constant used by the openweather plugin fix in PR #3442 for repo-wide consistency. Addresses part of issue #3455 (Bug 1). * fix(google): surface missing serpapi_api_key as a clear error Both `GoogleSearch._invoke` and `GoogleImageSearch._invoke` read `self.runtime.credentials["serpapi_api_key"]` directly. If the key is missing or empty, Python raises `KeyError: 'serpapi_api_key'` and the workflow run shows an opaque traceback instead of a friendly message. Add an early guard at the top of each `_invoke` that yields a clear "SerpAPI API key is required." text message and returns, and replace the `[]` lookup with `.get()` when populating `params["api_key"]`. Same pattern as the openweather plugin fix in PR #3442. Addresses the third part of issue #3455 (Bug 3). * test(google): cover invoke fall-through, timeouts, and missing-credential guards Adds `tools/google/tests/__init__.py` (empty) and `tools/google/tests/test_google.py`, a 7-case in-process test runner for both tools. The pattern matches the established `tools/notion/tests/test_retrieve_page.py` style adapted for stdlib only (no `pytest` dependency), using `unittest.mock.patch` to stub `requests.get`. The seven cases: - `test_google_search_invoke_missing_api_key_returns_message_and_no_http_call` - `test_google_image_search_invoke_missing_api_key_returns_message_and_no_http_call` - `test_google_image_search_invoke_invalid_hl_returns_message_and_no_http_call` - `test_google_image_search_invoke_invalid_gl_returns_message_and_no_http_call` - `test_google_image_search_invoke_success_yields_results` - `test_google_search_invoke_success_yields_results` - `test_requests_get_called_with_timeout_10` Run with: python3 tools/google/tests/test_google.py All 7 pass locally; both touched + new files compile under `python3 -m py_compile`. Covers each of the three bugs fixed in commits 1-3 plus the happy path. * chore(google): bump manifest version 0.1.5 -> 0.1.6 Patch bump per the repo PR template, reflecting the three reliability fixes (commit 1-3) plus the new in-process tests (commit 4). No `pyproject.toml` / `uv.lock` change.
Each guard in `_invoke()` yielded a friendly error message but did not
short-circuit afterwards, so the function continued executing and
posted the HTTP request anyway. `city=""` produced an OpenWeather 400
`{"message": "Nothing to geocode"}` body instead of the yield text;
missing `api_key` ran the same request with `appid=""` and returned
the cryptic `failed:401` body.
Add `return` after each guard so the generator exits cleanly with
the user-visible message.
Addresses part of issue langgenius#3441 (Bug 1).
Both `requests.get(...)` call sites — `query_weather()` and the tool `_invoke()` — had no `timeout=` kwarg. A hung OpenWeather host (or any TCP-accept-but-stall state) would block the Dify worker until the outer `MAX_REQUEST_TIMEOUT=120` ceiling, hiding a remote-host problem behind a request-timeout message. Pass `timeout=10` on both sites. 10 s is well above OpenWeather's normal p99 response time and well below Dify's 120 s outer ceiling, so it surfaces a clear connection-level error without escalating to the framework-level timeout. `query_weather()` exposes `timeout` as a keyword with a default of 10 so tests can stub a faster timeout. Addresses part of issue langgenius#3441 (Bug 2 / Bug 4).
…tial validator
`_validate_credentials` called `response.json().get("info")` on any
non-200 status. When OpenWeather returned a non-JSON failure page
(proxy 502 HTML, Cloudflare error, network-level boot screen), the
.json() call raised `JSONDecodeError` instead of surfacing the HTTP
status, and the validator crashed with an uninformative stack trace.
Same status-before-JSON ordering issue that PR langgenius#3431 just fixed for
the GitHub plugin: check the status first, then attempt to read the
body as JSON inside a `try/except ValueError` that falls back to a
status + 200-char snippet. On success the validator returns
silently (preserving the existing shape).
Also prefer the structured `info` field, fall back to `message`,
and only then to the raw text, so the user-visible error stays
useful for the common OpenWeather `{"cod":401,"message":"Invalid
API key"}` failure body.
Addresses part of issue langgenius#3441 (Bug 3).
…lidator status-before-json
Seven in-process tests exercise the three behavior fixes plus the
existing missing-api-key guard:
- `test_invoke_missing_city_returns_message_and_no_http_call`
guards empty `city` short-circuit (Bug 1) and asserts `requests.get`
was never called.
- `test_invoke_missing_api_key_returns_message_and_no_http_call`
guards missing `api_key` short-circuit (Bug 1) with the same no-HTTP
assertion.
- `test_invoke_success_yields_summary` locks the existing 200-response
shape so the fixes don't regress the happy path.
- `test_validate_credentials_http_400_with_html_body_surfaces_status`
reproduces the bug 3 regression: a non-JSON 4xx body must surface as
`ToolProviderCredentialValidationError` (no `JSONDecodeError`).
- `test_validate_credentials_http_401_surfaces_message` confirms the
new validator still surfaces OpenWeather's structured `message`
field when the body is valid JSON.
- `test_validate_credentials_missing_api_key_does_not_call_api`
preserves the existing behaviour where missing credentials fail
before any HTTP call.
- `test_requests_get_called_with_timeout_10` locks the new timeout
contract (Bug 2 / Bug 4) on `query_weather()`.
The test file stubs `dify_plugin`, `requests`, and a minimal
`pytest.raises` context manager (following the same pattern as
`tools/github/tests/test_refresh_credentials.py`), so the suite
runs in environments without the SDK or pytest installed. Run via:
cd tools/openweather && python3 -c "
import sys; sys.path.insert(0, 'tests')
import test_openweather as t
for n in sorted(n for n in dir(t) if n.startswith('test_')):
getattr(t, n)()
"
Add an empty `tests/__init__.py` to make `tools/openweather/tests`
discoverable in editors and CI imports.
Addresses the test coverage portion of issue langgenius#3441.
Patch bump per the PR template convention (MAJOR.MINOR.PATCH). 0.0.9 ships the three invoke/timeout/validator behavior fixes plus the new unit test suite. Leave `meta.version: 0.0.1` unchanged — that field is the runner SDK lock, not the user-facing release version.
… format
1. Change `sys.modules.setdefault("requests", _fake_requests)` to a direct assignment. The `setdefault` form was a no-op when pytest plugins (respx, syrupy, etc.) pre-loaded the real `requests` module, so the tool imported the real `requests` and the `patch.object` never reached it. Two tests (`test_invoke_success_yields_summary`, `test_requests_get_called_with_timeout_10`) were silently failing under pytest for this reason.
2. Apply `ruff format` to the test file and `tools/weather.py` (both had drifted out of the project format).
3. Add `# noqa: F401` to the `import tools.weather as tool_module` line, which is intentionally imported to ensure the module is loaded before `from tools.weather import OpenweatherTool`.
Verified: `pytest tests/` → 7 passed. `ruff check` → All checks passed.
3c5a8e9 to
22efb6f
Compare
|
Pushed a rebase plus one follow-up commit. The follow-up fixes a pre-existing bug in the test file: The Changed the stub to Now: |
Summary
Fixes #3441.
The
openweathertool plugin v0.0.8 has four real reliability bugs that are user-visible in any Dify installation: the tool keeps executing the HTTP request after yielding an error message, both outboundrequests.getcalls have notimeout=, and the credential validator crashes withJSONDecodeErroron any non-JSON failure page (e.g. a Cloudflare 502). This PR ships five small commits that fix all four bugs, add a test suite covering the new behavior, and bumps the plugin version.Change Type
Screenshots / Videos
city→ HTTP 400{"message":"Nothing to geocode"}city→ friendly"Please tell me your city", no HTTP callapi_key→ HTTP 401failed:401api_key→ friendly"OpenWeather API key is required.", no HTTP callJSONDecodeErrorstack traceOpenweather API rejected the key (HTTP 502): <snippet>LLM Plugin Checklist
N/A — this is a Non-LLM tool plugin.
Version
versioninmanifest.yaml(not the one undermeta)dify_plugin>=0.9.0is declared inpyproject.tomland locked inuv.lock— unchanged from v0.0.8; the plugin already declared>=0.9.0and this PR does not editpyproject.tomloruv.lockBump:
0.0.8→0.0.9(patch).Commits in this PR
1e23207efix(openweather): return after error yields in tool invocationc06d52e4fix(openweather): add 10s timeout to all requests.get callsbed926d2fix(openweather): check response status before parsing json in credential validator2d1f1babtest(openweather): cover invoke fall-through, timeouts, credential validator status-before-json8a4aaa83chore(openweather): bump manifest version to 0.0.9Testing
Local environment lacks
requestsandpytest, matching the convention used by other plugin test suites in this repo (e.g.tools/github/tests/test_refresh_credentials.py). The new test file stubs both.Verification command and result:
All seven cases pass:
test_invoke_missing_city_returns_message_and_no_http_calltest_invoke_missing_api_key_returns_message_and_no_http_calltest_invoke_success_yields_summarytest_validate_credentials_http_400_with_html_body_surfaces_statustest_validate_credentials_http_401_surfaces_messagetest_validate_credentials_missing_api_key_does_not_call_apitest_requests_get_called_with_timeout_10Local deployment — Dify version: latest main (1.7+); plugin test environment is the same minimal stub environment used by
tools/github/tests/test_refresh_credentials.pySaaS (cloud.dify.ai) — not applicable; this is a tool-side behavior change
Risks
_validate_credentials. The new wording includes the HTTP status and a 200-char body snippet, replacing the previousJSONDecodeErrorcrash. This is the desired UX.query_weather()keeps the same signature; thetimeoutkwarg has a default of 10 so the only call site (_validate_credentials) does not need updating.Files changed