filter(INTERSECTS(...)) produces malformed CQL — extra parens + TRUE AS "drop_null" artifacts cause WFS 400 on openmaps.gov.bc.ca
Summary
bcdata 0.5.2 generates CQL that GeoServer rejects with HTTP 400 / <ows:ExceptionReport exceptionCode="NoApplicableCode">, surfacing as the generic "There was an issue sending this WFS request" message from catch_wfs_error(). Affects any WFS query built via bcdc_query_geodata() |> filter(...).
Two distinct problems in the generated CQL:
1. Extra outer parentheses. A single-clause filter produces:
CQL_FILTER=((INTERSECTS(GEOMETRY, POLYGON ((...))))
instead of:
CQL_FILTER=INTERSECTS(GEOMETRY, POLYGON ((...)))
The server's CQL parser rejects the wrapped form.
2. TRUE AS "drop_null" SQL artifacts. Multi-clause filters leak a SQL-side TRUE AS "drop_null" placeholder into the CQL (CQL has no AS aliasing — that's SQL):
CQL_FILTER=((INTERSECTS(...)) AND TRUE AS "drop_null" AND ("BGC_LABEL" != 'ZZZ') AND TRUE AS "drop_null")
Minimal reproducer
library(bcdata); library(sf); library(dplyr)
## Any sf polygon over BC works; using a small bbox here for brevity.
geom <- st_as_sfc(st_bbox(c(xmin = 800000, ymin = 1200000, xmax = 850000, ymax = 1250000),
crs = st_crs(3005)))
## (1) Single filter clause -- fails because of extra outer parens
bcdc_query_geodata("f358a53b-ffde-4830-a325-a5a03ff672c3") |>
filter(INTERSECTS(geom)) |>
collect()
##> Error: There was an issue sending this WFS request
## (2) Same query via direct httr POST (no extra parens) returns 200:
httr::POST("https://openmaps.gov.bc.ca/geo/pub/wfs",
body = list(
SERVICE = "WFS", VERSION = "2.0.0", REQUEST = "GetFeature",
outputFormat = "application/json",
typeNames = "WHSE_FOREST_VEGETATION.BEC_BIOGEOCLIMATIC_POLY",
SRSNAME = "EPSG:3005",
CQL_FILTER = sprintf("INTERSECTS(GEOMETRY, %s)", st_as_text(geom[[1]]))
),
encode = "form"
) |> httr::status_code()
##> [1] 200
Server response (decoded)
catch_wfs_error() short-circuits on status ≥ 300 before parsing the response body, hiding the underlying diagnostic. Tracing bcdata:::bcdc_number_wfs_records() exposes the actual exception:
<?xml version="1.0" encoding="UTF-8"?>
<ows:ExceptionReport ... version="2.0.0">
<ows:Exception exceptionCode="NoApplicableCode">
<ows:ExceptionText></ows:ExceptionText>
</ows:Exception>
</ows:ExceptionReport>
The <ExceptionText> is empty, but the server's pre-exception log includes a Parsing : ((INTERSECTS(... line that pinpoints the outer wrapping as the trigger.
Suggested fix directions
- Drop the outermost
(...) wrap when there's only one CQL clause (or unconditionally — single-clause expressions don't need it).
- In the dplyr → CQL translator, strip columns of the form
TRUE AS "drop_null" before serialising. These look like dbplyr's "guarantee non-null projection" placeholders that aren't valid CQL.
Suggested UX improvement
catch_wfs_error() could surface the response body (or at least an <ows:ExceptionText> extract) in its error message — the current generic "There was an issue sending this WFS request" made the actual server diagnostic impossible to recover without trace()-ing internals.
Environment
bcdata 0.5.2 (CRAN, current)
R 4.6.0
Ubuntu 24.04 noble / 25.04 resolute
WFS endpoint: https://openmaps.gov.bc.ca/geo/pub/wfs (GeoServer-based)
filter(INTERSECTS(...))produces malformed CQL — extra parens +TRUE AS "drop_null"artifacts cause WFS 400 on openmaps.gov.bc.caSummary
bcdata 0.5.2generates CQL that GeoServer rejects with HTTP 400 /<ows:ExceptionReport exceptionCode="NoApplicableCode">, surfacing as the generic"There was an issue sending this WFS request"message fromcatch_wfs_error(). Affects any WFS query built viabcdc_query_geodata() |> filter(...).Two distinct problems in the generated CQL:
1. Extra outer parentheses. A single-clause filter produces:
instead of:
The server's CQL parser rejects the wrapped form.
2.
TRUE AS "drop_null"SQL artifacts. Multi-clause filters leak a SQL-sideTRUE AS "drop_null"placeholder into the CQL (CQL has noASaliasing — that's SQL):Minimal reproducer
Server response (decoded)
catch_wfs_error()short-circuits on status ≥ 300 before parsing the response body, hiding the underlying diagnostic. Tracingbcdata:::bcdc_number_wfs_records()exposes the actual exception:The
<ExceptionText>is empty, but the server's pre-exception log includes aParsing : ((INTERSECTS(...line that pinpoints the outer wrapping as the trigger.Suggested fix directions
(...)wrap when there's only one CQL clause (or unconditionally — single-clause expressions don't need it).TRUE AS "drop_null"before serialising. These look like dbplyr's "guarantee non-null projection" placeholders that aren't valid CQL.Suggested UX improvement
catch_wfs_error()could surface the response body (or at least an<ows:ExceptionText>extract) in its error message — the current generic"There was an issue sending this WFS request"made the actual server diagnostic impossible to recover withouttrace()-ing internals.Environment