Skip to content

fix(services): ask a WMS layer again in the next answer format after the service's error - #969

Merged
giswqs merged 1 commit into
opengeos:mainfrom
pick2510:fix/wms-info-fallback
Oct 26, 2026
Merged

giswqs merged 1 commit into
opengeos:mainfrom
pick2510:fix/wms-info-fallback

Conversation

@pick2510

@pick2510 pick2510 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #943 (Identify on WMS layers).

Problem

swisstopo's WMS (https://wms.geo.admin.ch/) lists application/json and application/json; subtype=geojson for GetFeatureInfo, so Identify asks in GeoJSON. For some layers its MapServer can't write GeoJSON and answers with an exception, e.g. Tectonic lines swissGEOCOVER2D (ch.swisstopo.geologie-swissgeocover2d_lines):

msOGRWriteFromQuery(): General error message. OGR OGR_DS_CreateLayer failed for layer
'ch.swisstopo.geologie-swissgeocover2d_gesteinhorizont' with driver 'GEOJSON'.
msApplySubstitutions(): Regular expression error. Parameter pattern validation failed.

The same point answers in GML (application/vnd.ogc.gml), text/xml and text/plain. Other layers of the same service (e.g. ch.bfs.gebaeude_wohnungs_register) answer in GeoJSON. Identify showed the error toast instead of the features.

Fix

  • rustgis-data (ogc_info.rs):
    • may_answer_in_another_format(body): the answer is an exception report (read with the same XML reader as the answers). Codes that no format changes don't count: LayerNotQueryable, LayerNotDefined, InvalidPoint, InvalidCRS/InvalidSRS, StyleNotDefined, dimension and update-sequence codes.
    • FeatureInfo::retry_after(request): the request in the formats read after the one the failed request asked in. Its INFO_FORMAT is matched against the layer's formats. Formats of the same kind go together, so both GeoJSON names are dropped at once.
    • FeatureInfo::answered(request): once a later format has answered, the layer asks in it.
  • identify.featureInfo:
    • Takes "url", the request that "text" answers. With lon and lat, a retryable error returns {tileLayer, url} with the next request.
    • The layer's own request changes only when that next format answers. A repeated or late answer can't drop a second format.
    • Without url or the point, or with no format left, the error is shown as before.
    • On the command line, the fetch loop retries the same way. Each retry asks in a format later in the order, so the loop ends.
  • App (mapview.rs): each pending answer keeps its request address and passes it with the point. A returned url already becomes the next background fetch, as with capabilities. While the retry is in flight, the popup keeps waiting.
  • Docs: docs/user-guide/editing.md, one paragraph.

The project format is unchanged: the request is runtime-only, as in #943, and a test checks this.

Tests

  • ogc_info: a_format_the_service_fails_in_gives_way_to_the_next covers:
    • MapServer's and OWS exceptions, with and without codes;
    • non-exceptions;
    • the order GeoJSON → GML → plain text → HTML, then none;
    • the format taken from the request rather than the layer;
    • answered.
  • Engine: identify_asks_again_in_the_next_format_after_the_services_error covers:
    • without the request or the point, the error as before;
    • the GML retry, with the layer keeping its format until GML answers;
    • the same error twice;
    • a GML answer settling the layer;
    • the last format's error;
    • a late GeoJSON answer not bringing GeoJSON back;
    • LayerNotQueryable and broken JSON not retried;
    • project file unchanged.
  • cargo xtask ci locally: fmt, clippy and layers pass, and 52 test suites pass. Only the two render tests that fail on this machine without its fonts fail (text_takes_italic_alignment_and_letter_spacing, a_text_graphic_is_drawn_aligned_slanted_and_spaced); they are unrelated.
  • Live, with rustgis-cli: identify on ch.swisstopo.geologie-swissgeocover2d_lines near Winterthur (8.729, 47.484) now returns the feature (kind_desc: Gesteinshorizont, lpro_litho_desc: Bentonit), and the next identify asks in GML straight away.

Summary by CodeRabbit

  • New Features
    • Feature information requests now retry using the next format offered by the service when an error indicates the requested format isn’t supported.
    • A successful fallback format is reused for that layer for the rest of the session.
    • Unrelated or malformed errors don’t trigger a retry.
  • Documentation
    • Updated the user guide to explain format retries and session behavior.

…the service's error

swisstopo's MapServer (wms.geo.admin.ch) offers GeoJSON for GetFeatureInfo
but fails to write it for some layers ("OGR_DS_CreateLayer failed … with
driver 'GEOJSON'", e.g. the swissGEOCOVER2D lines), while GML answers.
Identify showed the error instead of the features.

- An answer that is the service's exception report, without a code no
  format changes (LayerNotQueryable, InvalidPoint, …), gives the request in
  the next format the layer offers: both names of GeoJSON are left out
  together, then GML, other XML, plain text, HTML.
- The format comes from the request answered (its INFO_FORMAT), so a
  repeated or late answer doesn't drop another; the layer keeps its own
  until a later format answers, then asks in that one for the session.
- identify.featureInfo takes "url" (the request the text answers) with lon
  and lat; the app passes them, and turns the returned url into the next
  background fetch. On the command line the fetch loop does the same.
- Without url or the point, or with no format left, the error is told as
  before. Project files are unchanged.
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 2ca87f51-6402-45c4-bd5b-b8e978df70a9
Commits

Reviewing files that changed from the base of the PR and between a5aea60 and afab3c7.

Files selected for processing: 6
  • crates/data/src/ogc.rs
  • crates/data/src/ogc_info.rs
  • crates/engine/src/cmd/select_info.rs
  • crates/engine/src/tests.rs
  • crates/ui-egui/src/mapview.rs
  • docs/user-guide/editing.md

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Walkthrough

WMS feature-info handling now classifies eligible format errors, retries with later advertised formats, and retains a successful later format for the session. The map view passes request URLs and popup coordinates to the command when it processes feature-info answers.

Changes: WMS feature-info format fallback

Layer / File(s) Summary
Format ranking and retry rules
crates/data/src/ogc_info.rs, crates/data/src/ogc.rs
Feature-info formats use a shared ranking. XML exception reports are checked for eligibility before a retry. FeatureInfo can select a later format, and tests cover classification and format progression.
Feature-info command fallback
crates/engine/src/cmd/select_info.rs, crates/engine/src/tests.rs, docs/user-guide/editing.md
The command validates request URLs, retries eligible errors in later formats, and updates the selected format after a successful response. Tests and the guide describe the fallback behavior.
Map-view request URL tracking
crates/ui-egui/src/mapview.rs
Feature-info asks retain request URLs. The map view passes the URL and popup coordinates with each answer, then stores any returned follow-up URL.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant MapView
  participant IdentifyFeatureInfo
  participant WMS
  MapView->>IdentifyFeatureInfo: Send answer, request URL, and popup coordinates
  IdentifyFeatureInfo->>WMS: Request the next advertised format when eligible
  WMS->>IdentifyFeatureInfo: Return the format response
  IdentifyFeatureInfo->>MapView: Return results or a follow-up request URL
Loading

Possibly related PRs

  • opengeos/RustGIS#943: Introduced the feature-info types and request flow that this change extends.

Suggested reviewers: giswqs

Merge Risk: Minimal · up to afab3

The format fallback and map-view retry paths have no established merge-blocking issue. Merge after normal checks.

Pre-merge checks: Passed 4 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Docstring Coverage Inconclusive Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 4 files. (2 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: retrying a WMS layer with the next response format after a service error.
Description check Passed The description provides detailed problem, fix, testing, related issue, and validation information. It does not use the template's exact Summary or Related issue headings, but it includes the required…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description provides detailed problem, fix, testing, related issue, and validation information. It does not use the template's exact Summary or Related issue headings, but it includes the required content and is complete overall.

Full details: Docstring Coverage

Explanation

Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 4 files. (2 skipped: 1 unsupported, 1 too large.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks the formats in a row,
Then hops to the next when errors say so.
A GML reply is kept in view,
The map remembers the request URL too.
Soft paws celebrate the fallback flow.

Comment @coderabbitai help to get the list of available commands.

/// The answer format (one the layer offers) a request of the layer asked in: its INFO_FORMAT.
fn format_of(&self, request: &str) -> Option<&str> {
let asked = query_value(request, "info_format")?;
self.formats.iter().find(|f| super::encode(f).eq_ignore_ascii_case(asked)).map(String::as_str)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low (confidence: medium). answered trims formats to the format that answered and the ones after it, so after that format_of can no longer find the format of an earlier request. An error to a request still in flight in an earlier format (for example a GeoJSON request sent before the layer learned GML) then makes retry_after return None, and the error is shown instead of retried. A fix would keep the full list in formats and track the chosen format separately, or accept the stale error and say so in a test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining: the case can't come up. In the app, each popup asks a layer through one chain (request, then the retry after an error), and a new click replaces the popup, so the old chain's answers are dropped unread. The command line asks one request at a time. So no request in an earlier format is still in flight once the layer has moved to a later one, and format_of always finds the format of the request it is given.

if self.info_format().is_none_or(|f| format_rank(f) >= rank) {
return None;
}
let formats: Vec<String> = self.formats.iter().filter(|f| format_rank(f) >= rank).cloned().collect();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low (confidence: medium, design). One successful answer in a later format permanently drops every earlier format for the session. A transient service exception on GeoJSON (for example "Internal server error", which carries no code, so may_answer_in_another_format retries it) followed by a GML answer will move the layer to GML for good, even though GeoJSON would work again. This matches the stated goal of the PR, but it's worth deciding whether a retried success should only change the layer after it has been seen to recur, or whether the session-long switch is acceptable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intended: the layer moves to a later format only after its earlier format failed with the service's error and the later one answered, which is what this PR is for. It holds for the session only. A project reopened, or the layer added again, reads the capabilities and starts from the preferred format. Waiting for a second failure would mean every click on such a service makes a failing request first.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • Low: after a successful retry, answered trims formats, so format_of can no longer find the format of an earlier in-flight request. A late error to that request is shown rather than retried. Impact is small. Confidence: medium. (inline comment posted on ogc_info.rs:64)

Security

  • None found. The retry URL is built by ask, which keeps the address::check guard. The url parameter is only parsed for its format, never fetched in the text path. may_answer_in_another_format uses the existing bounded XML parser (nodes_limit). The retry and loop paths terminate, because each retry moves to a strictly higher format rank and there are at most five ranks. Confidence: high.

Performance

  • None significant. The extra XML parse runs only on the error path, and the answer is already capped at 16 MB.

Quality

  • Low (design): one later-format success permanently drops the earlier formats, even when the earlier error was transient (for example, a code-less "Internal server error" on GeoJSON). This matches the PR's stated intent, but the trade-off deserves a decision. Confidence: medium. (inline comment posted on ogc_info.rs:85)
  • Nits, not posted inline:
    • FeatureInfo::retry_after and answered clone self and then overwrite formats, which clones the list twice. Building the struct field by field would avoid this.
    • select_info.rs retry repeats the bounds check from next_fetch. Extracting a shared helper would remove the duplication.
    • In retry, the closure pattern |[w, s, e, n]| shadows the s: &Session parameter.

AGENTS.md

  • Checked and compliant: no unwrap, expect or indexing in non-test code; new tests are in test code; no std::time; no new UI literals, so nothing needs tl!; the xml nodes limit is set; the project format is unchanged, and feature_info is #[serde(skip)], which the test asserts. Confidence: high.
  • I could not build or run the tests in this environment. The results below are from the PR description and the diff.

I read the diff and the surrounding code in select_info.rs, mapview.rs, ogc_info.rs, ogc.rs and tiles.rs.

Change size

Compared with the base branch: the whole PR, merge base 0844c6e to head afab3c7.

Measure Value
Files changed 6 (0 added, 6 modified, 0 removed, 0 renamed)
Size before 1.43 MB
Size after 1.45 MB
Total size change +15.1 KB
Path Before After Change
crates/data/src/ogc_info.rs 35.0 KB 42.1 KB +7.0 KB
crates/engine/src/tests.rs 1.01 MB 1.02 MB +4.5 KB
crates/engine/src/cmd/select_info.rs 8.9 KB 11.7 KB +2.8 KB
crates/ui-egui/src/mapview.rs 187.0 KB 187.5 KB +497 B
docs/user-guide/editing.md 11.2 KB 11.4 KB +258 B
crates/data/src/ogc.rs 182.1 KB 182.1 KB +30 B

@github-actions

Copy link
Copy Markdown
Contributor

📖 Docs preview

Item Value
Preview https://preview.rustgis.app/pr-969/
Web app https://preview.rustgis.app/pr-969/app/
Commit afab3c7

GitHub Pages takes a minute or two to publish a new commit.

@giswqs
giswqs merged commit 889da8a into opengeos:main Oct 26, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants