Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

/api/streams omits "url" for pushed producers, making the whole response undecodable for strict clients

Open
#2,525 1 comment 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 2 days

@AlexxIT is already working on this.

Since Oct 1, 2026.

Assessment

This issue has not been assessed yet.

Description

enhancement

Summary

/api/streams omits "url" for pushed producers, making the whole response undecodable for strict clients
/api/streams can contain a producer object with no url key. This is not specific to
receivers — it happens for any producer that was pushed into a stream rather than dialled.
Clients that deserialize the response strictly then fail on the entire response, including
for unrelated streams.

Home Assistant's go2rtc-client is one such client: Producer.url is declared url: str with
no default, so one pushed producer anywhere makes streams.list() raise
mashumaro.exceptions.MissingField: Field "url" of type str is missing in Producer instance,
which in HA breaks every go2rtc-backed camera (snapshot and WebRTC offer paths).

Root cause

internal/streams/producer.go:

func (p *Producer) MarshalJSON() ([]byte, error) {
	if conn := p.conn; conn != nil {
		return json.Marshal(conn)     // p.url is discarded here
	}
	info := map[string]string{"url": p.url}
	return json.Marshal(info)
}

Once a producer is connected, the wrapper delegates serialization to the connection object and
drops p.url entirely. pkg/core/connection.go:42 then decides whether the key appears at all:

URL string `json:"url,omitempty"`

So a connection that never had SetURL() called emits no url key at all, while one that did
(rtsp, nest, webrtc dial) still shows it. That is why this looks intermittent and producer-specific
rather than systematic.

I think the intent was for it to always be reported. internal/streams/stream.go:

func (s *Stream) AddProducer(prod core.Producer) {
	producer := &Producer{conn: prod, state: stateExternal, url: "external"}

url: "external" is set so pushed producers can be identified — but per the MarshalJSON above it
is never emitted, because conn is always non-nil for these. It is currently a dead value.

Reproduction

streams:
  cam1:
    - tcp://127.0.0.1:65535
  1. Have an external process open a TCP connection to 127.0.0.1:65535 and write a raw H.264
    elementary stream. go2rtc accepts it as an external producer on that stream.
  2. Attach any client, e.g.
    ffmpeg -rtsp_transport tcp -i rtsp://127.0.0.1:8554/cam1
    (this is what adds the receivers array, but it is not required for the bug).
  3. GET /api/streams

Observed producer entry, no url key:

{
  "id": 1,
  "format_name": "h264",
  "medias": ["video, recvonly, H264"],
  "receivers": [
    { "id": 2, "codec": { "...": "..." }, "childs": [4],
      "bytes": 243052660, "packets": 19833 }
  ],
  "bytes_recv": 245298084
}

The sibling entry for the configured tcp:// source, in the same response, does have url —
because at that moment its conn is nil (the socket is not connected), so the fallback branch runs.

The same thing happens for WHIP/WebRTC push producers, which is the case reported on the Python
client side.

Note I did not build a minimal isolated repro for this — it was observed on a live go2rtc 1.9.14
instance running as a Home Assistant add-on, where a third-party integration publishes camera video
into a tcp:// stream.

Impact

The blast radius is wider than the affected stream. Any client that strictly deserializes
/api/streams gets nothing back, so a single pushed producer disables all streams, including
ones with no relation to it. In Home Assistant that means every go2rtc camera in the instance fails
while a publisher is connected — and because publishers connect and disconnect, it presents as an
intermittent failure that is hard to attribute.

Suggested fix

Merge the wrapper's URL into the marshaled connection, so the field is always present:

func (p *Producer) MarshalJSON() ([]byte, error) {
	if conn := p.conn; conn != nil {
		data, err := json.Marshal(conn)
		if err != nil {
			return nil, err
		}
		var m map[string]any
		if err = json.Unmarshal(data, &m); err != nil {
			return nil, err
		}
		if _, ok := m["url"]; !ok {
			m["url"] = p.url
		}
		return json.Marshal(m)
	}
	info := map[string]string{"url": p.url}
	return json.Marshal(info)
}

This also makes url: "external" in AddProducer meaningful again.

Dropping omitempty from core.Connection.URL would be a smaller change, but it would report
"url": "" rather than the actual source for pushed producers, and it would not fix conn
implementations that define their own MarshalJSON.

Making the field optional on the client side is a valid mitigation for a specific client, but it
leaves the API contract inconsistent and requires every strict client to harden independently.

Related

Dominant language
Go
Stars
14.3k
Forks
1.4k
Avg merge
1m
Merged PRs (30d)
1

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from AlexxIT/go2rtc

All issues in AlexxIT/go2rtc

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.