Skip to content

fix: create env on append in /update handler#1373

Open
tonypzy wants to merge 3 commits into
fossasia:devfrom
tonypzy:fix/775-update-append-new-env
Open

fix: create env on append in /update handler#1373
tonypzy wants to merge 3 commits into
fossasia:devfrom
tonypzy:fix/775-update-append-new-env

Conversation

@tonypzy

@tonypzy tonypzy commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Description

Motivation and Context

Fix #775

When /update is called with append=True for a not-yet-created environment, the handler crashes with KeyError (HTTP 500) at handler.state[eid]["jsons"] because it assumes the env already exists.

By adding:

if eid not in handler.state:
    handler.state[eid] = {"jsons": {}, "reload": {}}

register_window creates the window and broadcasts the env list as usual.

How Has This Been Tested?

Direct HTTP POST to /update with a non-existent environment.

Screenshots (if appropriate):

N/A

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Code refactor or cleanup (changes to existing code for improved readability or performance)

Checklist:

  • I adapted the version number under py/visdom/VERSION according to Semantic Versioning
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.

Summary by Sourcery

Bug Fixes:

  • Fix failure to create windows when appending in a previously unused environment by initializing the environment state before processing the update.

@sourcery-ai

sourcery-ai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Ensures that calling the /update endpoint with update='append' in a previously unused environment correctly initializes that environment and creates the target window instead of failing silently.

Sequence diagram for /update append creating environment on first use

sequenceDiagram
    actor Client
    participant Server
    participant HandlerState

    Client->>Server: POST /update(env=new_env, win=test, update=append)
    Server->>Server: update
    Server->>Server: extract_eid

    alt eid not in handler.state
        Server->>HandlerState: handler.state[eid] = {jsons: {}, reload: {}}
    end

    alt win not in handler.state[eid].jsons
        Server->>HandlerState: register_window
    else win exists
        Server->>HandlerState: append_to_window
    end

    Server-->>Client: response with created_or_updated_window
Loading

File-Level Changes

Change Details Files
Initialize handler state for a new environment ID before processing window updates so append-on-update can create windows in new environments.
  • Extract the environment ID from the request args at the start of the update handler wrapper.
  • Check whether the environment ID exists in handler.state and, if not, initialize it with empty jsons and reload dictionaries.
  • Rely on existing logic that creates a window on append when the window key is missing in the environment’s jsons to handle create-on-append semantics.
py/visdom/server/handlers/web_handlers.py

Assessment against linked issues

Issue Objective Addressed Explanation
#775 Ensure that calling vis.line(..., env='<new_env>', win='', update='append') creates the environment and window (with the plot) if they do not already exist, restoring the 0.1.8.8 create-on-append behavior.

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

@tonypzy tonypzy requested review from Manik-Khajuria-5 and rajnisht7 and removed request for Manik-Khajuria-5 June 3, 2026 23:08

@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 left some high level feedback:

  • Consider factoring out the handler.state[eid] = {"jsons": {}, "reload": {}} initialization into a shared helper or using the same code path as other env-creation logic to avoid duplicating the default structure and keep behavior consistent if it changes in the future.
  • If handler.state can be accessed concurrently (e.g., multiple requests for the same eid), you may want to guard the if eid not in handler.state block or use a safer pattern (e.g., setdefault) to avoid potential race conditions or partial initialization.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider factoring out the `handler.state[eid] = {"jsons": {}, "reload": {}}` initialization into a shared helper or using the same code path as other env-creation logic to avoid duplicating the default structure and keep behavior consistent if it changes in the future.
- If `handler.state` can be accessed concurrently (e.g., multiple requests for the same `eid`), you may want to guard the `if eid not in handler.state` block or use a safer pattern (e.g., `setdefault`) to avoid potential race conditions or partial initialization.

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.

@tonypzy

tonypzy commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@rajnisht7 rajnisht7 requested a review from Copilot June 4, 2026 05:10

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds initialization of per-environment state in the web update handler to avoid missing-key errors when handling requests for a new eid.

Changes:

  • Initialize handler.state[eid] when an eid is first encountered.
  • Ensures jsons and reload dictionaries exist before accessing window state.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread py/visdom/server/handlers/web_handlers.py
Comment thread py/visdom/server/handlers/web_handlers.py

@Manik-Khajuria-5 Manik-Khajuria-5 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@tonypzy LGTM!

@rajnisht7 rajnisht7 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.

@tonypzy tested the reproduction steps from the original issue, but worked fine appending to a window in a new env successfully created both env and the window, and returned the expected window id. can you share code/steps to reproduce it

@tonypzy

tonypzy commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

@tonypzy tested the reproduction steps from the original issue, but worked fine appending to a window in a new env successfully created both env and the window, and returned the expected window id. can you share code/steps to reproduce it

Try this.

import json

payload = {
    "win": "test",
    "eid": "main2",
    "append": True,
    "data": [{"x": [1, 2], "y": [0, 2], "type": "scatter", "mode": "lines", "name": "1"}],
    "layout": {},
    "opts": {},
}

r = requests.post("http://localhost:8097/update", data=json.dumps(payload))
print(f"Status: {r.status_code}")
print(f"Body: {r.text[:200]!r}")

@tonypzy tonypzy changed the title fix: create env on append for /update so create-on-append works in new envs fix: create env on append in /update handler Jun 6, 2026
@rajnisht7

Copy link
Copy Markdown
Contributor

@tonypzy is this problem limited to direct /update API usage

@tonypzy

tonypzy commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

@tonypzy is this problem limited to direct /update API usage

It is not limited to direct /update calls. It solves a speical usage case.

@rajnisht7

rajnisht7 commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

@tonypzy
tested with

import numpy as np
from visdom import Visdom

viz = Visdom()
assert viz.check_connection(), "Visdom server not running!"

viz.line(X=[1, 2], Y=[0, 2], env='testing11', win='test123', update='append')

viz.line(X=[1, 2], Y=[2, 2], env='testing9', win='test123', update='append')

and ensured that both envs testing11 and testing9 doesnt exist also ensured there is no pane having win test123 but it worked
so can you kindly have a look in it or share code to reproduce it as shared in issue

@tonypzy

tonypzy commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

@tonypzy tested with

import numpy as np
from visdom import Visdom

viz = Visdom()
assert viz.check_connection(), "Visdom server not running!"

viz.line(X=[1, 2], Y=[0, 2], env='testing11', win='test123', update='append')

viz.line(X=[1, 2], Y=[2, 2], env='testing9', win='test123', update='append')

and ensured that both envs testing11 and testing9 doesnt exist also ensured there is no pane having win test123 but it worked so can you kindly have a look in it or share code to reproduce it as shared in issue

This PR fixes the path where the client is bypassed and an /update (append) call is made directly to a non-existent env. During a viz.line call, the client first checks win_exists to decide whether to route to /events or /update, so it never actually hits this block of code. \update is a public endpoint, so a valid request shouldn't 500

@tonypzy tonypzy requested a review from rajnisht7 June 8, 2026 02:06

@rajnisht7 rajnisht7 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.

@tonypzy as per the ci logs

Checking for scripts.
INFO:root:Server started
INFO:root:Application Started
INFO:root:Working directory: /home/acer/.visdom
You can navigate to http://localhost:8097
INFO:tornado.access:200 POST /env/main (::1) 0.37ms
INFO:tornado.access:101 GET /vis_socket (::1) 0.34ms
INFO:root:Opened visdom source socket from ip: ::1
INFO:tornado.access:200 POST /win_exists (::1) 238.08ms
INFO:tornado.access:200 POST /win_exists (::1) 0.24ms
INFO:tornado.access:200 POST /win_exists (::1) 0.15ms
INFO:tornado.access:200 POST /events (::1) 0.21ms

whenever we pass update append for non-existent wins or envs python automatically hits /win_exists first and routes to /events
also the endpoint this PR address is never meant to be called directly nor we recommend to use it that way
that is

requests.post("http://localhost:8097/update", data=json.dumps({
    "win": "test",
    "eid": "main2",   # non-existent env
    "append": True,
    ...
}))

even though the fix is harmless but it addresses an endpoint that no real user calls directly
can you share a concrete real world scenario where someone would bypass the client and hit /update with a non-existent env?

@tonypzy

tonypzy commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

@tonypzy as per the ci logs

Checking for scripts.
INFO:root:Server started
INFO:root:Application Started
INFO:root:Working directory: /home/acer/.visdom
You can navigate to http://localhost:8097
INFO:tornado.access:200 POST /env/main (::1) 0.37ms
INFO:tornado.access:101 GET /vis_socket (::1) 0.34ms
INFO:root:Opened visdom source socket from ip: ::1
INFO:tornado.access:200 POST /win_exists (::1) 238.08ms
INFO:tornado.access:200 POST /win_exists (::1) 0.24ms
INFO:tornado.access:200 POST /win_exists (::1) 0.15ms
INFO:tornado.access:200 POST /events (::1) 0.21ms

whenever we pass update append for non-existent wins or envs python automatically hits /win_exists first and routes to /events also the endpoint this PR address is never meant to be called directly nor we recommend to use it that way that is

requests.post("http://localhost:8097/update", data=json.dumps({
    "win": "test",
    "eid": "main2",   # non-existent env
    "append": True,
    ...
}))

even though the fix is harmless but it addresses an endpoint that no real user calls directly can you share a concrete real world scenario where someone would bypass the client and hit /update with a non-existent env?

Actually, lines 534 and 599 in web_handler.py already use a similar {"jsons": {}, "reload": {}} structure. Rather than "patching a dead endpoint," this change simply aligns UpdateHandler with existing conventions in the codebase.

Regarding the practical application, there is a race condition during concurrent execution. If an env is deleted after win_exists returns True but before the /update request is processed, it throws a 500 error. The code added in this PR rebuilds the env in this exact scenario, preventing the crash

@tonypzy tonypzy requested a review from rajnisht7 June 11, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Behaviour of vis.line(update='append') in visdom 0.1.8.9

6 participants