Skip to content

fix(openweather): invoke fall-through, request timeouts, and credential validator status-before-json - #3442

Open
Harsh23Kashyap wants to merge 6 commits into
langgenius:mainfrom
Harsh23Kashyap:fix/openweather-invoke-and-timeouts
Open

fix(openweather): invoke fall-through, request timeouts, and credential validator status-before-json#3442
Harsh23Kashyap wants to merge 6 commits into
langgenius:mainfrom
Harsh23Kashyap:fix/openweather-invoke-and-timeouts

Conversation

@Harsh23Kashyap

Copy link
Copy Markdown
Contributor

Summary

Fixes #3441.

The openweather tool 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 outbound requests.get calls have no timeout=, and the credential validator crashes with JSONDecodeError on 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

  • Documentation / non-plugin change
  • Non-LLM plugin (tools, extensions, datasource, etc.)
  • LLM plugin

Screenshots / Videos

Before After
Empty city → HTTP 400 {"message":"Nothing to geocode"} Empty city → friendly "Please tell me your city", no HTTP call
Missing api_key → HTTP 401 failed:401 Missing api_key → friendly "OpenWeather API key is required.", no HTTP call
Hung OpenWeather host → 120 s Dify timeout Hung host → clear error within ~12 s
Non-JSON 502 → JSONDecodeError stack trace Non-JSON 502 → Openweather API rejected the key (HTTP 502): <snippet>

LLM Plugin Checklist

N/A — this is a Non-LLM tool plugin.

Version

  • Bumped top-level version in manifest.yaml (not the one under meta)
  • dify_plugin>=0.9.0 is declared in pyproject.toml and locked in uv.lock — unchanged from v0.0.8; the plugin already declared >=0.9.0 and this PR does not edit pyproject.toml or uv.lock

Bump: 0.0.80.0.9 (patch).

Commits in this PR

# SHA Subject
1 1e23207e fix(openweather): return after error yields in tool invocation
2 c06d52e4 fix(openweather): add 10s timeout to all requests.get calls
3 bed926d2 fix(openweather): check response status before parsing json in credential validator
4 2d1f1bab test(openweather): cover invoke fall-through, timeouts, credential validator status-before-json
5 8a4aaa83 chore(openweather): bump manifest version to 0.0.9

Testing

Local environment lacks requests and pytest, 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:

cd tools/openweather
python3 -m py_compile provider/openweather.py
python3 -m py_compile tools/weather.py
python3 -m py_compile tests/test_openweather.py

python3 -c "
import sys; sys.path.insert(0, 'tests')
import test_openweather as t
for n in sorted(x for x in dir(t) if x.startswith('test_')):
    getattr(t, n)()
print('all 7 tests pass')
"

All seven cases pass:

  • test_invoke_missing_city_returns_message_and_no_http_call

  • test_invoke_missing_api_key_returns_message_and_no_http_call

  • test_invoke_success_yields_summary

  • test_validate_credentials_http_400_with_html_body_surfaces_status

  • test_validate_credentials_http_401_surfaces_message

  • test_validate_credentials_missing_api_key_does_not_call_api

  • test_requests_get_called_with_timeout_10

  • Local deployment — Dify version: latest main (1.7+); plugin test environment is the same minimal stub environment used by tools/github/tests/test_refresh_credentials.py

  • SaaS (cloud.dify.ai) — not applicable; this is a tool-side behavior change

Risks

  • User-visible error wording changes for the rare case where OpenWeather returns a non-JSON failure body in _validate_credentials. The new wording includes the HTTP status and a 200-char body snippet, replacing the previous JSONDecodeError crash. This is the desired UX.
  • Successful tool responses are byte-identical (no response-shape change on the 200 path).
  • query_weather() keeps the same signature; the timeout kwarg has a default of 10 so the only call site (_validate_credentials) does not need updating.

Files changed

tools/openweather/provider/openweather.py   | 18 +++++++++++++++++--
tools/openweather/tests/__init__.py         |  0 (new, empty)
tools/openweather/tests/test_openweather.py | 319 ++++++++++++++++++++++++++++ (new)
tools/openweather/tools/weather.py          |  4 ++--
tools/openweather/manifest.yaml             |  2 +-
5 files changed, 339 insertions(+), 5 deletions(-)

@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. bug Something isn't working labels Jul 15, 2026
crazywoola pushed a commit that referenced this pull request Jul 20, 2026
…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.
@Harsh23Kashyap
Harsh23Kashyap force-pushed the fix/openweather-invoke-and-timeouts branch from 3c5a8e9 to 22efb6f Compare July 27, 2026 15:38
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Jul 27, 2026
@Harsh23Kashyap

Copy link
Copy Markdown
Contributor Author

Pushed a rebase plus one follow-up commit. The follow-up fixes a pre-existing bug in the test file:

The requests stub used sys.modules.setdefault("requests", _fake_requests). Under pytest, several plugins (respx, pytest-recording, syrupy, etc.) pre-load the real requests module before the test file runs, so setdefault was a no-op. The tool ended up importing the real requests, the patch.object(real_requests, "get", rec) never reached the tool, and two tests (test_invoke_success_yields_summary, test_requests_get_called_with_timeout_10) were silently failing.

Changed the stub to sys.modules["requests"] = _fake_requests (direct assignment). Also applied ruff format and added a # noqa: F401 on the intentional import tools.weather line.

Now: pytest tests/ → 7 passed. ruff check → All checks passed. Branch is 0 behind upstream/main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[openweather] Tool falls through after error yields; missing timeouts on requests.get; status-before-JSON in credential validator

1 participant