Skip to content

Add custom GeoJSON map support to choropleth visualizations#7639

Open
elipollak wants to merge 2 commits intogetredash:masterfrom
elipollak:agent/resource-proxy-endpoint
Open

Add custom GeoJSON map support to choropleth visualizations#7639
elipollak wants to merge 2 commits intogetredash:masterfrom
elipollak:agent/resource-proxy-endpoint

Conversation

@elipollak
Copy link

@elipollak elipollak commented Feb 24, 2026

What type of PR is this?

  • Feature

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

  • Map type dropdown: "Custom..." option in the choropleth editor
  • URL input: Text field for GeoJSON URL (debounced), with error display for failed loads
  • Direct browser fetch: axios.get(url) with reference-counting cache
  • Error handling: Clear message when fetch fails, including CORS guidance
  • fieldNames support: Reads fieldNames foreign member from GeoJSON for human-readable labels in Key/Value dropdowns
  • @@name alias: Ensures default tooltip template works with any GeoJSON regardless of property naming

CSP note

Admins who want to use external GeoJSON URLs need to allow outbound fetches in their Content Security Policy, e.g.:

REDASH_CONTENT_SECURITY_POLICY="default-src 'self'; connect-src 'self' https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'"

The URL input shows a clear error if the fetch is blocked by CSP or CORS.

How is this tested?

  • Unit tests (pytest, jest)
  • E2E Tests (Cypress)
  • Manually

Jest: tests for getOptions (custom map defaults, validation skip), prepareFeatureProperties (@@name aliasing), getGeoJsonFields, fieldNames mapping (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
Pasted Graphic 5

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.
Pasted Graphic 7

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.
Pasted Graphic 8

Related Tickets & Documents

Previous version of this PR included a backend proxy; removed per reviewer feedback.

@elipollak elipollak force-pushed the agent/resource-proxy-endpoint branch 2 times, most recently from 85167aa to 400736f Compare February 24, 2026 14:22
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

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-ai with guidance or docs links (including llms.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.

"User-Agent": "Redash",
},
)
response.raise_for_status()
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 24, 2026

Choose a reason for hiding this comment

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

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>
Fix with Cubic

Copy link
Author

Choose a reason for hiding this comment

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

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.

@elipollak elipollak force-pushed the agent/resource-proxy-endpoint branch from 400736f to b60bcf0 Compare February 24, 2026 14:25
@yoshiokatsuneo
Copy link
Contributor

Thank you for the contribution.
I could not get why we need proxy. How about just downloading json on frontend ?

@elipollak
Copy link
Author

elipollak commented Feb 25, 2026

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:

"Allowing requests to be triggered by JS code sounds like a potential risk, and the kind of risk CSP supposed to prevent."
@arikfr suggested the proxy approach here.

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:

  • Proxy approach (this PR): no CSP changes needed, but adds a backend endpoint
  • Frontend-only + Redash adds connect-src 'self' https: to the default CSP: simpler, no backend code, limits to HTTPS only. This is narrower than the previously discussed connect-src * since it still blocks non-HTTPS schemes
  • Frontend-only, each Redash admin configures CSP: no default change, admins who want custom maps add their trusted hosts to CSP (e.g. connect-src 'self' https://raw.githubusercontent.com) via REDASH_CONTENT_SECURITY_POLICY
  • Alternatively, people could add additional geojson maps to the Redash build as discussed here, but I think that seemed less preferred based on some of the closed PRs to add other maps

Let me know what you think? Happy to rework this based on your feedback

@yoshiokatsuneo
Copy link
Contributor

yoshiokatsuneo commented Feb 25, 2026

In that PR, @kravets-levko proposed adding connect-src * to CSP to allow direct browser fetches, but #4599 (comment):

"Allowing requests to be triggered by JS code sounds like a potential risk, and the kind of risk CSP supposed to prevent."
@arikfr suggested the proxy approach #4599 (comment)

@elipollak

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)...
( For about cookies, nowadays, third party cookies from ajax requests are already blocked in most cases.)
(CC: @arikfr )

@elipollak
Copy link
Author

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:

  • Add connect-src 'self' https: to Redash's default CSP (works out of the box, HTTPS only)
  • Leave CSP as-is and let admins configure it (zero default change, but custom maps won't work without config)

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>
@elipollak elipollak force-pushed the agent/resource-proxy-endpoint branch from b60bcf0 to d0c824f Compare February 25, 2026 08:17
@elipollak
Copy link
Author

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/:

  • Custom map type option in the choropleth editor dropdown
  • URL input with debounce and error display
  • fieldNames support for human-readable target field labels
  • @@name aliasing so tooltips work with any GeoJSON
  • I've manually tested the full workflow (custom Kenya counties GeoJSON, error handling for invalid URLs, switching between custom and built-in maps) and they all working.

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>
@elipollak elipollak force-pushed the agent/resource-proxy-endpoint branch from 4f5ed65 to b61637a Compare February 25, 2026 11:00
@yoshiokatsuneo
Copy link
Contributor

Thank you.
Now, I'm sorry but I'm not very sure what is the best way.
I think there are options like below.

  1. Volume mount maps on starting docker container. This will work now, but not user friendly.

  2. Add custom geojson URL in each Reash queries. Frontend JavaScript load the map.
    Disable content-security-policy connect-src for all domains.

  3. Add custom geojson URL in each Reash queries. Frontend JavaScript load the map.
    Disable content-security-policy connect-src for specific domains, using environment variables.

  4. Add custom geojson URL in each Redash queries, and add a backend proxy interface that accept any URL.
    It allow users to access any URLs through backend API.

  5. Add custom geojson URL in each Redash queries, and add a proxy interface that accept visualization id.
    By this, backend only access the URL that user explicitly specified for each query.

  6. Add geojson for the countries like Kenya in this case to the source code repository.
    This is the way we have for the maps in USA or Japan.

  7. Add new tables to manage geojson URLs, and users can specify map by name on each query.
    I think this is the way metabase custom maps does.

I feel "7" may be the best among this, but it requires some a bit of changes on the code.
I would like to ask to other's thought, too. ( @eradman @arikfr )

@elipollak
Copy link
Author

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!

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