Skip to content

feat: Add dynamic JSON progress bar.#205

Merged
guibranco merged 4 commits into
guibranco:mainfrom
nimble-turtle:json-dynamic
Apr 14, 2026
Merged

feat: Add dynamic JSON progress bar.#205
guibranco merged 4 commits into
guibranco:mainfrom
nimble-turtle:json-dynamic

Conversation

@nimble-turtle

@nimble-turtle nimble-turtle commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Closes #204

📑 Description

Adds the ability to have progress bars that retrieve dynamic JSON.

It adds quite a few lines of code, so I understand if this feels like too much of a change. I also added quite a number of tests because pulling in remote files increases the attack surface both for your Vercel server and the end user.

Updated the README.md to reflect the new API usage info.

It adds one new dependency for JSON parsing.

Let me know if you'd like any edits.

✅ Checks

  • My pull request adheres to the code style of this project
  • My code requires changes to the documentation
  • I have updated the documentation as required
  • All the tests have passed

☢️ Does this introduce a breaking change?

  • Yes
  • No

Summary by Sourcery

Add support for rendering progress bar SVGs from values loaded dynamically from remote JSON documents, with robust validation and security safeguards.

New Features:

  • Expose a new /dynamic/json/ endpoint that renders progress bar SVGs based on values extracted from a remote JSON URL via JSONPath or dot notation queries.

Bug Fixes:

  • Ensure SVG responses consistently set a strict Content-Type and X-Content-Type-Options header to prevent content sniffing and improve safety.
  • Escape title values in the SVG markup to avoid script injection in rendered progress bars.

Enhancements:

  • Introduce helper utilities for securely fetching and parsing remote JSON, including URL validation, size limits, and content type checks, to reduce SSRF and injection risk when loading dynamic data.

Build:

  • Add jsonpath-ng as a new dependency for JSONPath query support.

Documentation:

  • Document the new JSON-based progress endpoint, its parameters, and provide an example JSON file and usage snippet in the README.

Tests:

  • Add extensive tests covering the dynamic JSON endpoint behavior, including JSONPath handling, error conditions, cache headers, and security-related URL and content-type restrictions, as well as tests for SVG escaping and headers.

Summary by CodeRabbit

Release Notes

  • New Features

    • New endpoint for dynamically generating progress bars from remote JSON data sources with JSONPath/dot notation query support and configurable caching.
    • Added documentation describing remote JSON configuration, query parameters, and styling options.
  • Tests

    • Expanded test coverage for security headers and endpoint validation behaviors.

@vercel

vercel Bot commented Apr 13, 2026

Copy link
Copy Markdown

@nimble-turtle is attempting to deploy a commit to the guibranco's projects Team on Vercel.

A member of the Team first needs to authorize it.

@sourcery-ai

sourcery-ai Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements a new /dynamic/json/ endpoint that safely fetches remote JSON, extracts a numeric progress value via JSONPath, and renders the existing SVG progress bar, along with SSRF/content-type safeguards, caching controls, tests, and documentation updates.

Sequence diagram for dynamic JSON progress bar endpoint

sequenceDiagram
  actor User
  participant Browser
  participant FlaskApp
  participant FetchJsonOpener
  participant RemoteJsonServer
  participant JsonPathNg
  participant Jinja2

  User->>Browser: Request /dynamic/json/?url=...&query=...
  Browser->>FlaskApp: HTTP GET /dynamic/json/
  FlaskApp->>FlaskApp: Validate url and query params
  alt Missing or disallowed url or query
    FlaskApp->>Jinja2: Render dynamic_error.svg
    Jinja2-->>FlaskApp: SVG error content
    FlaskApp-->>Browser: 400/502/422 SVG error
    Browser-->>User: Error progress bar image
  else Allowed url and query
    FlaskApp->>FetchJsonOpener: fetch_json_document(url)
    FetchJsonOpener->>RemoteJsonServer: HTTP GET JSON
    RemoteJsonServer-->>FetchJsonOpener: JSON response
    FetchJsonOpener-->>FlaskApp: Parsed JSON document
    FlaskApp->>JsonPathNg: extract_progress_with_jsonpath(doc, query)
    JsonPathNg-->>FlaskApp: Numeric progress value
    FlaskApp->>FlaskApp: get_template_fields(progress)
    FlaskApp->>Jinja2: Render progress.svg
    Jinja2-->>FlaskApp: SVG progress bar
    FlaskApp->>FlaskApp: Apply _finalize_svg_response and cache headers
    FlaskApp-->>Browser: 200 SVG response
    Browser-->>User: Dynamic JSON progress bar image
  end
Loading

Class diagram for no-redirect HTTP handler used in JSON fetching

classDiagram
  class HTTPRedirectHandler {
  }

  class _NoHttpRedirectHandler {
    +redirect_request(req, fp, code, msg, headers, newurl)
  }

  _NoHttpRedirectHandler --|> HTTPRedirectHandler
Loading

File-Level Changes

Change Details Files
Add secure dynamic JSON-backed progress endpoint that renders SVG using extracted numeric value.
  • Introduce /dynamic/json/ Flask route that accepts url, query, and optional cache/cacheSeconds parameters and reuses get_template_fields to render progress.svg
  • Implement JSON fetching pipeline with bounded size, timeout, strict HTTP(S)-only URLs, standard ports, and a custom opener that disallows redirects to reduce SSRF risk
  • Add JSONPath-based value extraction supporting both JSONPath and dot-path syntax, including type validation and percent-suffix parsing for strings
  • Return SVG error responses with appropriate status codes (400/422/502) using a dedicated dynamic_error.svg template and helper function
app.py
templates/dynamic_error.svg
Harden SVG responses and template rendering for all progress endpoints.
  • Add Jinja2 autoescaping configuration to ensure HTML/JS in titles and other fields are escaped in SVG output
  • Refactor SVG response creation into a _finalize_svg_response helper that sets Content-Type to image/svg+xml and X-Content-Type-Options=nosniff, and use it in existing numeric endpoint
app.py
test_api.py
Introduce JSON fetch content-type and size validation helpers to guard against non-JSON and overly large responses.
  • Add _content_type_allowed_for_json_fetch with a blocklist of non-JSON/active content MIME type prefixes
  • Limit JSON response body to 512 KiB and raise errors for larger responses before parsing
  • Decode response as UTF-8 and parse via json.loads with error handling
app.py
test_api.py
Add SSRF-focused URL validation for remote JSON fetches.
  • Implement url_is_allowed_for_fetch that validates scheme, host, port, userinfo, URL length, and blocks localhost/metadata/IP ranges including private, loopback, link-local, reserved, and multicast
  • Resolve DNS for the host and re-check resulting IPs against the same blocked categories
app.py
test_api.py
Extend automated tests and documentation for dynamic JSON progress support.
  • Add unit tests covering SVG headers, escaping behavior, dynamic JSON happy-path scenarios, cache headers, invalid input handling, SSRF protections, and fetch_json_document content-type behavior using mocks
  • Document the new JSON-based progress feature in README with example URL, parameter table, and reference sample JSON file
  • Add a sample JSON document for documentation/demo purposes and introduce jsonpath-ng as a new dependency
test_api.py
README.md
docs/dynamic-json-sample.json
requirements.txt

Assessment against linked issues

Issue Objective Addressed Explanation
#204 Add a feature that can fetch a remote JSON document by URL, extract a numeric progress value using a selector, and render it as an SVG progress bar.
#204 Support a selector syntax like progress.0.data.approvalProgress (as well as JSONPath-style selectors) to locate the percentage value in the JSON document.
#204 Document the new dynamic JSON progress bar feature and its usage in the README.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gstraccini gstraccini Bot added enhancement New feature or request 🚦 awaiting triage Items that are awaiting triage or categorization labels Apr 13, 2026
@github-actions github-actions Bot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Apr 13, 2026
@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@nimble-turtle has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 36 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 44 minutes and 36 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bccc57db-c6ef-44b8-b396-2d2bee118ddb

📥 Commits

Reviewing files that changed from the base of the PR and between 249f5bd and d25c65e.

📒 Files selected for processing (3)
  • README.md
  • app.py
  • test_api.py

Walkthrough

The pull request introduces a new /dynamic/json/ endpoint enabling progress bar generation from remote JSON data sources. Users can specify a URL and JSONPath/dot-notation selector to extract numeric progress values, with built-in SSRF protections, JSON validation, and caching support.

Changes

Cohort / File(s) Summary
Documentation
README.md, docs/dynamic-json-sample.json
Added "Progress from a JSON URL" guide documenting the new /dynamic/json/ endpoint with query parameters (url, query, optional cache) and included a sample JSON file for reference.
Core Feature Implementation
app.py
Introduced /dynamic/json/ route handler with URL allowlisting (SSRF protection), HTTP(S) JSON fetching with content-type validation, JSONPath/dot-notation query normalization and extraction, error SVG rendering, and updated the existing /<int:progress>/ route to use centralized response finalization.
Dependencies
requirements.txt
Added jsonpath-ng==1.7.0 library for JSON path query support.
Test Coverage
test_api.py
Expanded test suite with security header assertions (X-Content-Type-Options), comprehensive /dynamic/json/ parameter validation, error condition handling (missing params, blocked URLs, fetch failures, invalid queries), JSON content-type enforcement, caching behavior verification, and JSONPath/dot-path extraction logic.

Sequence Diagram

sequenceDiagram
    actor Client
    participant App as App Server
    participant HTTP as HTTP Fetcher
    participant Extractor as JSONPath Extractor
    participant Renderer as SVG Renderer

    Client->>App: GET /dynamic/json/?url=...&query=...
    App->>App: Validate & normalize URL (SSRF check)
    App->>App: Normalize JSONPath/dot-notation query
    App->>HTTP: Fetch JSON document (with timeout/size limits)
    HTTP-->>App: JSON document (with Content-Type check)
    App->>Extractor: Extract numeric value using selector
    Extractor-->>App: Parsed numeric progress value
    App->>Renderer: Render progress.svg with extracted value
    Renderer-->>App: SVG response with Content-Type headers
    App-->>Client: SVG image + cache headers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A little rabbit hops with glee,
For JSON data flows so free,
Through selectors, paths do wind,
Progress bars of every kind,
Security checks keep hackers out,
That's what remote data's all about! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main feature addition: dynamic JSON progress bar support, which matches the primary objective from issue #204 and the substantial changes across documentation, routes, and tests.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #204 [#204]: dynamic JSON fetching via URL, JSONPath/dot-notation selectors, percentage extraction, and rendering as progress bars, with comprehensive security, error handling, and test coverage.
Out of Scope Changes check ✅ Passed All changes remain within the scope of issue #204 [#204]. The security features (SSRF protections, URL allowlisting, HTTP redirect blocking) and SVG response refactoring are supporting infrastructure directly necessary for secure remote JSON fetching.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

@deepsource-io

deepsource-io Bot commented Apr 13, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in f689a45...d25c65e on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Secrets Apr 13, 2026 8:36p.m. Review ↗
Python Apr 13, 2026 8:36p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@gstraccini gstraccini Bot requested a review from guibranco April 13, 2026 20:21
@penify-dev

penify-dev Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Failed to generate code suggestions for PR

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - I've found 6 issues, and left some high level feedback:

  • In get_progress_svg_dynamic_json, _finalize_svg_response(response) is called without using its return value, which is a bit confusing given that the function returns the response; consider either returning its result directly (as in get_progress_svg) or making _finalize_svg_response mutate in place and return None for consistency.
  • In fetch_json_document, the response body is always decoded as UTF-8 regardless of any charset in the Content-Type header; if you expect external APIs with other encodings, you may want to respect the charset parameter or at least fail fast when a different charset is declared.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `get_progress_svg_dynamic_json`, `_finalize_svg_response(response)` is called without using its return value, which is a bit confusing given that the function returns the response; consider either returning its result directly (as in `get_progress_svg`) or making `_finalize_svg_response` mutate in place and return `None` for consistency.
- In `fetch_json_document`, the response body is always decoded as UTF-8 regardless of any charset in the `Content-Type` header; if you expect external APIs with other encodings, you may want to respect the charset parameter or at least fail fast when a different charset is declared.

## Individual Comments

### Comment 1
<location path="app.py" line_range="414-415" />
<code_context>
+  template_fields = get_template_fields(progress)
+  template = render_template("progress.svg", **template_fields)
+  response = make_response(template)
+  _finalize_svg_response(response)
+  cache_raw = request.args.get("cache") or request.args.get("cacheSeconds")
+  if cache_raw is not None:
</code_context>
<issue_to_address>
**suggestion:** Use the return value of `_finalize_svg_response` to avoid coupling to its mutating behavior.

Currently `_finalize_svg_response` mutates and returns the same response, so ignoring the return value happens to work. To avoid depending on that implementation detail and stay consistent with `get_progress_svg`, please assign the result:

```python
esponse = _finalize_svg_response(response)
```

This prevents future bugs if `_finalize_svg_response` is changed to return a new response object.

```suggestion
  response = make_response(template)
  response = _finalize_svg_response(response)
```
</issue_to_address>

### Comment 2
<location path="app.py" line_range="136" />
<code_context>
+  except ValueError:
+    pass
+  try:
+    for res in socket.getaddrinfo(host, None):
+      addr = res[4][0]
+      try:
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Consider also blocking multicast addresses in the DNS resolution branch for consistency with the literal-IP checks.

Allowing hostnames that resolve to multicast addresses (e.g. 224.x.x.x or ff00::/8) while blocking them for literal IPs creates an inconsistent policy and may open an SSRF vector in some environments.

You can mirror the literal-IP check here by including `ip.is_multicast`:

```python
if (
    ip.is_private
    or ip.is_loopback
    or ip.is_link_local
    or ip.is_reserved
    or ip.is_multicast
):
    return False
```

Suggested implementation:

```python
    if (
      ip.is_private
      or ip.is_loopback
      or ip.is_link_local
      or ip.is_reserved
      or ip.is_multicast
    ):
      return False

```

If the literal-IP branch already includes `ip.is_multicast`, no further changes are needed there. Ensure the `SEARCH` block above only matches the DNS-resolution branch (i.e., the one where `ip` is derived from `socket.getaddrinfo` results, such as `addr`), not the literal-IP parsing branch which should already contain `or ip.is_multicast`.
</issue_to_address>

### Comment 3
<location path="test_api.py" line_range="21" />
<code_context>
+def test_svg_response(client):
</code_context>
<issue_to_address>
**suggestion (testing):** Add an assertion that the dynamic JSON endpoint also sets `X-Content-Type-Options` to `nosniff`.

Since `/dynamic/json/` also returns SVG via `_finalize_svg_response`, please add a dedicated test (e.g. `test_dynamic_json_sets_nosniff_header`) that hits `/dynamic/json/` with a mocked `fetch_json_document` and asserts `X-Content-Type-Options == 'nosniff'` to guard against regressions.
</issue_to_address>

### Comment 4
<location path="test_api.py" line_range="52-61" />
<code_context>
+    assert b"73" in response.data
+
+
+@patch("app.fetch_json_document")
+def test_dynamic_json_jsonpath_dollar(mock_fetch, client):
+    mock_fetch.return_value = {"items": [{"n": 42}]}
+    response = client.get(
+        "/dynamic/json/",
+        query_string={
+            "url": "https://example.com/x.json",
+            "query": "$.items[0].n",
+        },
+    )
+    assert response.status_code == 200
+    assert b"42" in response.data
+
+
+@patch("app.fetch_json_document")
+def test_dynamic_json_percent_suffix_string(mock_fetch, client):
+    mock_fetch.return_value = {"approvalProgress": "73%"}
+    response = client.get(
</code_context>
<issue_to_address>
**suggestion (testing):** Cover additional `extract_progress_with_jsonpath` string-parsing edge cases (empty and non-numeric strings).

Please also add tests where the JSONPath result is (1) an empty or whitespace-only string (expect 422 with an "empty" error) and (2) a non-numeric string like "foo" (expect 422 with a "not numeric" error), so the branches raising "matched string is empty" and "matched string is not numeric" are covered.
</issue_to_address>

### Comment 5
<location path="test_api.py" line_range="217-36" />
<code_context>
+    assert response.status_code == 400
+
+
+def test_dynamic_json_nonstandard_port_blocked(client):
+    response = client.get(
+        "/dynamic/json/",
+        query_string={
+            "url": "https://example.com:8080/data.json",
+            "query": "$.x",
+        },
+    )
+    assert response.status_code == 400
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Extend URL-allowlist tests to cover long URLs and cloud-metadata hosts that are explicitly blocked.

Current tests nicely cover localhost, schemes, userinfo, and nonstandard ports. Since `url_is_allowed_for_fetch` also enforces a max URL length and blocks cloud-metadata hosts (e.g., `metadata.google.internal`, `169.254.169.254`), please add tests asserting that: (1) a URL longer than `JSON_URL_MAX_LENGTH` returns 400, and (2) requests to those metadata endpoints also return 400, so these protections are explicitly verified.

Suggested implementation:

```python
import pytest
from app import app, fetch_json_document, JSON_URL_MAX_LENGTH

```

```python
def test_dynamic_json_nonstandard_port_blocked(client):
    response = client.get(
        "/dynamic/json/",
        query_string={
            "url": "https://example.com:8080/data.json",
            "query": "$.x",
        },
    )
    assert response.status_code == 400


def test_dynamic_json_url_too_long_blocked(client):
    # Construct a URL longer than JSON_URL_MAX_LENGTH
    excessive_length = JSON_URL_MAX_LENGTH + 10
    long_path = "a" * excessive_length
    long_url = f"https://example.com/{long_path}.json"

    response = client.get(
        "/dynamic/json/",
        query_string={
            "url": long_url,
            "query": "$.x",
        },
    )
    assert response.status_code == 400


@pytest.mark.parametrize(
    "metadata_url",
    [
        "http://169.254.169.254/latest/meta-data",
        "http://metadata.google.internal/computeMetadata/v1",
    ],
)
def test_dynamic_json_cloud_metadata_hosts_blocked(client, metadata_url):
    response = client.get(
        "/dynamic/json/",
        query_string={
            "url": metadata_url,
            "query": "$.x",
        },
    )
    assert response.status_code == 400

```
</issue_to_address>

### Comment 6
<location path="test_api.py" line_range="228-237" />
<code_context>
+    assert response.status_code == 400
+
+
+@patch("app._fetch_json_opener.open")
+def test_fetch_json_rejects_html_content_type(mock_open):
+    mock_resp = MagicMock()
+    mock_resp.headers.get.return_value = "text/html; charset=utf-8"
+    mock_resp.read.return_value = b"{}"
+    ctx = MagicMock()
+    ctx.__enter__.return_value = mock_resp
+    ctx.__exit__.return_value = None
+    mock_open.return_value = ctx
+    with pytest.raises(ValueError, match="unsupported content type"):
+        fetch_json_document("https://example.com/x.json")
+
+
+@patch("app._fetch_json_opener.open")
+def test_fetch_json_allows_application_json(mock_open):
+    mock_resp = MagicMock()
+    mock_resp.headers.get.return_value = "application/json; charset=utf-8"
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for `fetch_json_document` error branches: oversized responses, invalid UTF-8, and malformed JSON.

Currently only content-type rejection and happy-path JSON are tested. Please also cover the remaining error paths by mocking `read` to: (1) return `JSON_FETCH_MAX_BYTES + 1` bytes and assert `ValueError('response too large')`; (2) return invalid UTF-8 bytes and (3) syntactically invalid JSON (e.g. `b'{"n": }'`) and assert these are surfaced by the dynamic JSON view as 422 responses. This will ensure the size limit and parse failures are exercised end-to-end.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread app.py Outdated
Comment thread app.py
Comment thread test_api.py
assert response.headers.get("X-Content-Type-Options") == "nosniff"

assert b"<svg" in response.data
assert b"</svg>" in response.data

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.

suggestion (testing): Add an assertion that the dynamic JSON endpoint also sets X-Content-Type-Options to nosniff.

Since /dynamic/json/ also returns SVG via _finalize_svg_response, please add a dedicated test (e.g. test_dynamic_json_sets_nosniff_header) that hits /dynamic/json/ with a mocked fetch_json_document and asserts X-Content-Type-Options == 'nosniff' to guard against regressions.

Comment thread test_api.py
Comment on lines +52 to +61
@patch("app.fetch_json_document")
def test_dynamic_json_query_param(mock_fetch, client):
mock_fetch.return_value = {"v": 9}
response = client.get(
"/dynamic/json/",
query_string={
"url": "https://example.com/x.json",
"query": "$.v",
},
)

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.

suggestion (testing): Cover additional extract_progress_with_jsonpath string-parsing edge cases (empty and non-numeric strings).

Please also add tests where the JSONPath result is (1) an empty or whitespace-only string (expect 422 with an "empty" error) and (2) a non-numeric string like "foo" (expect 422 with a "not numeric" error), so the branches raising "matched string is empty" and "matched string is not numeric" are covered.

Comment thread test_api.py

def test_dynamic_json_missing_params(client):
response = client.get("/dynamic/json/")
assert response.status_code == 400

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.

suggestion (testing): Extend URL-allowlist tests to cover long URLs and cloud-metadata hosts that are explicitly blocked.

Current tests nicely cover localhost, schemes, userinfo, and nonstandard ports. Since url_is_allowed_for_fetch also enforces a max URL length and blocks cloud-metadata hosts (e.g., metadata.google.internal, 169.254.169.254), please add tests asserting that: (1) a URL longer than JSON_URL_MAX_LENGTH returns 400, and (2) requests to those metadata endpoints also return 400, so these protections are explicitly verified.

Suggested implementation:

import pytest
from app import app, fetch_json_document, JSON_URL_MAX_LENGTH
def test_dynamic_json_nonstandard_port_blocked(client):
    response = client.get(
        "/dynamic/json/",
        query_string={
            "url": "https://example.com:8080/data.json",
            "query": "$.x",
        },
    )
    assert response.status_code == 400


def test_dynamic_json_url_too_long_blocked(client):
    # Construct a URL longer than JSON_URL_MAX_LENGTH
    excessive_length = JSON_URL_MAX_LENGTH + 10
    long_path = "a" * excessive_length
    long_url = f"https://example.com/{long_path}.json"

    response = client.get(
        "/dynamic/json/",
        query_string={
            "url": long_url,
            "query": "$.x",
        },
    )
    assert response.status_code == 400


@pytest.mark.parametrize(
    "metadata_url",
    [
        "http://169.254.169.254/latest/meta-data",
        "http://metadata.google.internal/computeMetadata/v1",
    ],
)
def test_dynamic_json_cloud_metadata_hosts_blocked(client, metadata_url):
    response = client.get(
        "/dynamic/json/",
        query_string={
            "url": metadata_url,
            "query": "$.x",
        },
    )
    assert response.status_code == 400

Comment thread test_api.py
Comment on lines +228 to +237
@patch("app._fetch_json_opener.open")
def test_fetch_json_rejects_html_content_type(mock_open):
mock_resp = MagicMock()
mock_resp.headers.get.return_value = "text/html; charset=utf-8"
mock_resp.read.return_value = b"{}"
ctx = MagicMock()
ctx.__enter__.return_value = mock_resp
ctx.__exit__.return_value = None
mock_open.return_value = ctx
with pytest.raises(ValueError, match="unsupported content type"):

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.

suggestion (testing): Add tests for fetch_json_document error branches: oversized responses, invalid UTF-8, and malformed JSON.

Currently only content-type rejection and happy-path JSON are tested. Please also cover the remaining error paths by mocking read to: (1) return JSON_FETCH_MAX_BYTES + 1 bytes and assert ValueError('response too large'); (2) return invalid UTF-8 bytes and (3) syntactically invalid JSON (e.g. b'{"n": }') and assert these are surfaced by the dynamic JSON view as 422 responses. This will ensure the size limit and parse failures are exercised end-to-end.

@codacy-production

codacy-production Bot commented Apr 13, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 minor

Alerts:
⚠ 2 issues (≤ 0 issues of at least minor severity)

Results:
2 new issues

Category Results
Documentation 2 minor

View in Codacy

🟢 Metrics 7 duplication

Metric Results
Duplication 7

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

@socket-security

socket-security Bot commented Apr 13, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedjsonpath-ng@​1.7.0100100100100100

View full report

@socket-security

socket-security Bot commented Apr 13, 2026

Copy link
Copy Markdown

Caution

Review the following alerts detected in dependencies.

According to your organization's Security Policy, you must resolve all "Block" alerts before proceeding. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Block Low
Filesystem access: pypi jsonpath-ng

Location: Package overview

From: requirements.txtpypi/jsonpath-ng@1.7.0

ℹ Read more on: This package | This alert | What is filesystem access?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: If a package must read the file system, clarify what it will read and ensure it reads only what it claims to. If appropriate, packages can leave file system access to consumers and operate on data passed to it instead.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore pypi/jsonpath-ng@1.7.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Block Low
Embedded URLs or IPs: pypi jsonpath-ng

URLs: https://github.com/kennknowles/python-jsonpath-rw, https://github.com/h2non/jsonpath-ng, https://goessner.net/articles/JsonPath/index.html#e3

Location: Package overview

From: requirements.txtpypi/jsonpath-ng@1.7.0

ℹ Read more on: This package | This alert | What are URL strings?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Review all remote URLs to ensure they are intentional, pointing to trusted sources, and not being used for data exfiltration or loading untrusted code at runtime.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore pypi/jsonpath-ng@1.7.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Block Low
Embedded URLs or IPs: pypi jsonpath-ng

URLs: https://github.com/kennknowles/python-jsonpath-rw

Location: Package overview

From: requirements.txtpypi/jsonpath-ng@1.7.0

ℹ Read more on: This package | This alert | What are URL strings?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Review all remote URLs to ensure they are intentional, pointing to trusted sources, and not being used for data exfiltration or loading untrusted code at runtime.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore pypi/jsonpath-ng@1.7.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@nimble-turtle

Copy link
Copy Markdown
Contributor Author

That is an impressive number of AI review bots! 😂

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (3)
app.py (2)

154-166: URL scheme validation relies on caller; consider defensive check.

Static analysis (S310) flags _fetch_json_opener.open(req, ...) since urllib can open file:// URLs. While url_is_allowed_for_fetch() validates schemes before this function is called, fetch_json_document() itself doesn't enforce the constraint. A defensive scheme check here would prevent misuse if called from other code paths in the future.

Proposed defensive check
 def fetch_json_document(url):
+  parsed = urlparse(url)
+  if parsed.scheme not in ("http", "https"):
+    raise ValueError("only http/https URLs are supported")
   req = Request(
     url,
     headers={"User-Agent": JSON_FETCH_USER_AGENT},
     method="GET",
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app.py` around lines 154 - 166, Add a defensive URL-scheme check at the start
of fetch_json_document to ensure callers cannot pass disallowed schemes (e.g.,
file://); call the existing url_is_allowed_for_fetch(url) (or validate
urllib.parse.urlparse(url).scheme against the allowed list) and raise
ValueError("unsupported URL scheme") if it returns false before constructing the
Request and calling _fetch_json_opener.open, so fetch_json_document enforces the
same scheme restrictions as url_is_allowed_for_fetch.

95-151: SSRF protections are comprehensive but note DNS rebinding TOCTOU window.

The URL validation is thorough: scheme restrictions, blocked hosts, IP range checks, port limits, userinfo rejection, and DNS resolution checks. However, there's a time-of-check-to-time-of-use (TOCTOU) window between socket.getaddrinfo() here and the actual HTTP connection in fetch_json_document(). An attacker with control over DNS could return a public IP during validation, then a private IP during the actual fetch (DNS rebinding).

The risk is mitigated by:

  1. The _NoHttpRedirectHandler blocking redirects
  2. The short time window between check and fetch

For additional hardening, consider binding the resolved IP directly when making the request, though this adds complexity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app.py` around lines 95 - 151, The current SSRF check in
url_is_allowed_for_fetch has a TOCTOU gap between socket.getaddrinfo() and the
actual HTTP request in fetch_json_document(); fix by moving to a model where
fetch_json_document resolves the host once and connects directly to the
validated IP (rather than allowing the HTTP client to re-resolve), i.e., have
fetch_json_document call the same _ip_for_ssrf_check/socket.getaddrinfo logic
used in url_is_allowed_for_fetch to obtain a concrete IP, verify that IP remains
allowed, open the TCP connection to that IP (using the numeric address) while
setting the HTTP Host header to the original hostname to preserve virtual host
semantics, and ensure redirects are still blocked (keep _NoHttpRedirectHandler).
This prevents DNS rebinding between validation and use without changing
url_is_allowed_for_fetch semantics.
test_api.py (1)

165-177: Consider testing edge cases for cache parameter.

The implementation has if sec > 0 to skip setting Cache-Control for zero values, and clamps to 86400 max. Consider adding tests for these edge cases.

Suggested additional tests
`@patch`("app.fetch_json_document")
def test_dynamic_json_cache_zero_no_header(mock_fetch, client):
    """cache=0 should not set Cache-Control header."""
    mock_fetch.return_value = {"n": 5}
    response = client.get(
        "/dynamic/json/",
        query_string={
            "url": "https://example.com/x.json",
            "query": "$.n",
            "cache": "0",
        },
    )
    assert response.status_code == 200
    assert "Cache-Control" not in response.headers


`@patch`("app.fetch_json_document")
def test_dynamic_json_cache_clamped_to_max(mock_fetch, client):
    """cache values exceeding 86400 should be clamped."""
    mock_fetch.return_value = {"n": 5}
    response = client.get(
        "/dynamic/json/",
        query_string={
            "url": "https://example.com/x.json",
            "query": "$.n",
            "cache": "999999",
        },
    )
    assert response.status_code == 200
    assert response.headers.get("Cache-Control") == "public, max-age=86400"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test_api.py` around lines 165 - 177, Add two edge-case tests for the dynamic
JSON cache behavior: create test_dynamic_json_cache_zero_no_header and
test_dynamic_json_cache_clamped_to_max (both patching app.fetch_json_document
like the existing test_dynamic_json_cache_sets_cache_control) that call
client.get("/dynamic/json/") with query params "url", "query" and "cache" set to
"0" and a very large value (e.g. "999999") respectively; assert the zero case
returns status 200 and does NOT include "Cache-Control" in response.headers, and
assert the large-value case returns status 200 and includes "Cache-Control"
equal to "public, max-age=86400" to verify the if sec > 0 behavior and the 86400
clamp in the code handling cache.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 208: Replace the incorrect opening HTML tag "<tbody>" at the reported
location with the closing tag "</tbody>" so the table markup is properly closed;
locate the stray "<tbody>" in README.md (the same incorrect tag referenced in
the comment) and change it to "</tbody>" to fix the HTML typo.
- Line 162: Replace the incorrect opening HTML tag at the end of the table:
change the stray `<tbody>` token to the closing tag `</tbody>` so the table body
is properly terminated; locate the fragment containing `<tbody>` in README.md
and update it to `</tbody>` (ensure matching with surrounding `<table>` /
`<thead>` tags).

---

Nitpick comments:
In `@app.py`:
- Around line 154-166: Add a defensive URL-scheme check at the start of
fetch_json_document to ensure callers cannot pass disallowed schemes (e.g.,
file://); call the existing url_is_allowed_for_fetch(url) (or validate
urllib.parse.urlparse(url).scheme against the allowed list) and raise
ValueError("unsupported URL scheme") if it returns false before constructing the
Request and calling _fetch_json_opener.open, so fetch_json_document enforces the
same scheme restrictions as url_is_allowed_for_fetch.
- Around line 95-151: The current SSRF check in url_is_allowed_for_fetch has a
TOCTOU gap between socket.getaddrinfo() and the actual HTTP request in
fetch_json_document(); fix by moving to a model where fetch_json_document
resolves the host once and connects directly to the validated IP (rather than
allowing the HTTP client to re-resolve), i.e., have fetch_json_document call the
same _ip_for_ssrf_check/socket.getaddrinfo logic used in
url_is_allowed_for_fetch to obtain a concrete IP, verify that IP remains
allowed, open the TCP connection to that IP (using the numeric address) while
setting the HTTP Host header to the original hostname to preserve virtual host
semantics, and ensure redirects are still blocked (keep _NoHttpRedirectHandler).
This prevents DNS rebinding between validation and use without changing
url_is_allowed_for_fetch semantics.

In `@test_api.py`:
- Around line 165-177: Add two edge-case tests for the dynamic JSON cache
behavior: create test_dynamic_json_cache_zero_no_header and
test_dynamic_json_cache_clamped_to_max (both patching app.fetch_json_document
like the existing test_dynamic_json_cache_sets_cache_control) that call
client.get("/dynamic/json/") with query params "url", "query" and "cache" set to
"0" and a very large value (e.g. "999999") respectively; assert the zero case
returns status 200 and does NOT include "Cache-Control" in response.headers, and
assert the large-value case returns status 200 and includes "Cache-Control"
equal to "public, max-age=86400" to verify the if sec > 0 behavior and the 86400
clamp in the code handling cache.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bdb1e247-4a9b-45d8-b6fd-da16a87981c4

📥 Commits

Reviewing files that changed from the base of the PR and between f689a45 and 249f5bd.

⛔ Files ignored due to path filters (1)
  • templates/dynamic_error.svg is excluded by !**/*.svg
📒 Files selected for processing (5)
  • README.md
  • app.py
  • docs/dynamic-json-sample.json
  • requirements.txt
  • test_api.py

Comment thread README.md Outdated
Comment thread README.md Outdated
nimble-turtle and others added 3 commits April 13, 2026 13:25
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@gstraccini gstraccini Bot added hacktoberfest Participation in the Hacktoberfest event ✨ feature New feature requests or implementations 🎨 ux User experience enhancements or design-related work 📝 documentation Tasks related to writing or updating documentation 📦 dependencies Dependencies 🔍 under investigation Issue or bug report is under investigation 🕔 high effort A task that can be completed in a few days 🧪 tests Tasks related to testing labels Apr 14, 2026
@guibranco

Copy link
Copy Markdown
Owner

Hi @nimble-turtle,

Thank you so much for your pull request! 🙌

I appreciate the time and effort you put into this contribution.
I'll review it shortly, and if everything looks good, I'll approve it as soon as possible.

Thanks again for your valuable contribution! 🚀

@vercel

vercel Bot commented Apr 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
progressbar Ready Ready Preview, Comment Apr 14, 2026 1:59pm

@guibranco guibranco merged commit edc9fb2 into guibranco:main Apr 14, 2026
16 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚦 awaiting triage Items that are awaiting triage or categorization 📦 dependencies Dependencies 📝 documentation Tasks related to writing or updating documentation enhancement New feature or request ✨ feature New feature requests or implementations hacktoberfest Participation in the Hacktoberfest event 🕔 high effort A task that can be completed in a few days size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. 🧪 tests Tasks related to testing 🔍 under investigation Issue or bug report is under investigation 🎨 ux User experience enhancements or design-related work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Add ability to dynamically pull data from JSON file using a selector.

2 participants