Add custom GeoJSON map support to choropleth visualizations#7639
Add custom GeoJSON map support to choropleth visualizations#7639elipollak wants to merge 2 commits intogetredash:masterfrom
Conversation
85167aa to
400736f
Compare
There was a problem hiding this comment.
1 issue found across 18 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="redash/handlers/geojson_proxy.py">
<violation number="1" location="redash/handlers/geojson_proxy.py:59">
P2: Streamed response isn’t closed on error paths; if raise_for_status or other RequestException occurs, the connection stays open and can exhaust the requests connection pool under repeated failures.</violation>
</file>
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Add one-off context when rerunning by tagging
@cubic-dev-aiwith guidance or docs links (includingllms.txt) - Ask questions if you need clarification on any suggestion
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
redash/handlers/geojson_proxy.py
Outdated
| "User-Agent": "Redash", | ||
| }, | ||
| ) | ||
| response.raise_for_status() |
There was a problem hiding this comment.
P2: Streamed response isn’t closed on error paths; if raise_for_status or other RequestException occurs, the connection stays open and can exhaust the requests connection pool under repeated failures.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At redash/handlers/geojson_proxy.py, line 59:
<comment>Streamed response isn’t closed on error paths; if raise_for_status or other RequestException occurs, the connection stays open and can exhaust the requests connection pool under repeated failures.</comment>
<file context>
@@ -0,0 +1,93 @@
+ "User-Agent": "Redash",
+ },
+ )
+ response.raise_for_status()
+ except UnacceptableAddressException:
+ logger.warning("GeoJSON proxy: blocked private address for URL: %s", url)
</file context>
There was a problem hiding this comment.
Thank you - fixed. The streamed response is now wrapped in a with response: context manager so the connection is always closed on all code paths.
400736f to
b60bcf0
Compare
|
Thank you for the contribution. |
|
Thanks you for the review and question @yoshiokatsuneo! This approach was based on the partial work in #4599 by @kravets-levko (with the groundwork merged in #5186). In that PR, @kravets-levko proposed adding connect-src * to CSP to allow direct browser fetches, but @arikfr suggested a proxy instead:
As I understand it, Redash's default CSP has default-src 'self' with no connect-src override, so browser JS can't fetch external URLs, which is why we went with the proxy approach. That said, I think there are a few ways to handle this:
Let me know what you think? Happy to rework this based on your feedback |
Thank you! I think adding proxy layer does not change the risk so much, as it anyway allow to connect to arbitrary URL (from frontend or from backend)... |
|
Thanks for the quick response! That is a very fair point. I was trying to follow the guidance from the earlier PR discussion, but agree the simpler client-side approach could work well. Did you have a view on CSP changes to enable this? The current default CSP's default-src 'self' would block browser fetches to external URLs. Options would be:
Either way, I'll start reworking it as frontend-only |
Users can now select "Custom" as a map type and provide any public GeoJSON URL to render as a choropleth map. Frontend: Custom map type option in editor, URL input with debounced updates, error display for failed loads, fieldNames support for human-readable labels from GeoJSON foreign members, @@name alias ensures default tooltip template works with any GeoJSON regardless of property naming. Note: Admins need to configure their CSP to allow external fetches (e.g. connect-src 'self' https: via REDASH_CONTENT_SECURITY_POLICY). Tests: viz-lib Jest tests for getOptions, prepareFeatureProperties, getGeoJsonFields, fieldNames mapping. Cypress E2E test for custom map editor workflow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
b60bcf0 to
d0c824f
Compare
|
Hey @yoshiokatsuneo! I've reworked this as frontend-only per your feedback. GeoJSON is now fetched directly from the browser via axios.get(url) with a reference-counting cache, and the backend proxy endpoint is completely removed. The diff is now much simpler — just frontend changes in viz-lib/:
One open question from our earlier thread: would you be comfortable with adding connect-src 'self' https: to the default CSP so this works out of the box? Without it, admins need to manually configure REDASH_CONTENT_SECURITY_POLICY before custom maps will load. Adding it would limit external fetches to HTTPS only, which seems like a reasonable default, but happy to leave CSP untouched if you'd prefer the opt-in approach. |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4f5ed65 to
b61637a
Compare
|
Thank you.
I feel "7" may be the best among this, but it requires some a bit of changes on the code. |
|
Thank you for laying out all the options, super helpful! I wonder whether it might make sense to start with option 3 (what we have now), since it would mean that custom maps would only work on instances where admins explicitly choose to open up CSP via environment variables, making it opt-in. Then option 7 could be a natural follow-up, and the frontend changes here (GeoJSON loading, @@name mapping, Custom map type, etc) would carry over directly since the visualization code would be very similar/same either way. Very happy to hear what @eradman and @arikfr think makes the most sense! |
What type of PR is this?
Description
Adds support for custom GeoJSON map boundaries in choropleth visualizations. Users can select "Custom..." as a map type and provide any public GeoJSON URL.
Update (v2): Based on @yoshiokatsuneo's review feedback, the backend proxy (
/api/geojson-proxy) has been removed entirely. GeoJSON is now fetched directly from the browser. This eliminates SSRF risk and simplifies the implementation to frontend-only changes.Changes
axios.get(url)with reference-counting cachefieldNamessupport: ReadsfieldNamesforeign member from GeoJSON for human-readable labels in Key/Value dropdowns@@namealias: Ensures default tooltip template works with any GeoJSON regardless of property namingCSP note
Admins who want to use external GeoJSON URLs need to allow outbound fetches in their Content Security Policy, e.g.:
The URL input shows a clear error if the fetch is blocked by CSP or CORS.
How is this tested?
Jest: tests for
getOptions(custom map defaults, validation skip),prepareFeatureProperties(@@namealiasing),getGeoJsonFields,fieldNamesmapping (179 tests, 25 suites pass).Cypress: E2E test for custom map editor workflow — select Custom, enter URL, verify paths render, switch back to built-in map.
Manual test results (v2 — direct browser fetch)
All tests performed on a clean local dev environment (Docker + webpack dev server).
1. Map type dropdown with "Custom..." option

2. Kenya counties choropleth rendered from custom GeoJSON URL

Custom GeoJSON URL loaded from GitHub Gist, with Key Column → county, Target Field → NAME_1, Value Column → pct_customized_blend. Counties render with data-driven color scale.
3. Error handling for invalid/unreachable URL

Entering an invalid URL shows red "Network Error" text below the input. Map preview clears. Target Field dropdown resets.
Related Tickets & Documents
Previous version of this PR included a backend proxy; removed per reviewer feedback.