Skip to content

Multiple commands return exit 0 with empty stdout when output serialization fails, silently swallowing successful API responses #740

@hoyt-harness

Description

@hoyt-harness

Body

Version: gws v0.22.5 (Windows x86_64 binary from GitHub Releases)

Environment:

  • OS: Windows 10/11
  • Invocation: cmd /c gws ...
  • Auth: OAuth user token via gws auth login --scopes <explicit URI list>
  • Shared Drive in use: Yes

Summary

Several gws commands exit with code 0 and no stderr error, but write nothing to stdout, on API calls that should return data. Cross-checks against the same Workspace account via Google's APIs and via alternative gws command forms confirm the underlying data exists — the response is being produced by the API and then silently dropped by gws before reaching stdout.

This is distinct from legitimate empty-result cases (where gws correctly writes {} or an empty items array).

Affected endpoints (confirmed)

All reproduced against the same authenticated session.

Endpoint Symptom Cross-check confirming data exists
gws people people connections list empty stdout, exit 0 Account has contacts — confirmed via people contactGroups get with maxMembers returning populated memberResourceNames
gws people people searchContacts empty stdout, exit 0 searchContacts with a known contact email returns empty even though the contact exists
gws gmail users labels list empty stdout, exit 0 8 user labels exist (verified independently)
gws gmail +read --message-id <ID> empty stdout, exit 0; stderr notes "unknown output format 'text', falling back to json" but nothing follows Same message ID returns full content via gws gmail users messages get
gws gmail users messages get for a specific message ID empty stdout, exit 0 Same command with a different message ID returns the full multipart message correctly

Reproduction

Case A: gmail users labels list

gws gmail users labels list --params "{\"userId\":\"me\"}"

Expected: JSON object containing labels array with all user and system labels.

Actual: Exit 0. Empty stdout. Stderr shows only the cosmetic Using keyring backend: keyring line.

Case B: gmail users messages get — ID-specific

gws gmail users messages get --params "{\"userId\":\"me\",\"id\":\"<id>\",\"format\":\"full\"}"

Expected: Full message resource (headers, body parts, snippet).

Actual: Exit 0. Empty stdout.

Contrast: The same command with a different message ID (from the same account, same token) returns a complete multipart message resource. The only difference is the message ID.

Case C: gmail +read

gws gmail +read --message-id <id>

Expected: Message text rendered to stdout.

Actual: Exit 0. Empty stdout. Stderr includes unknown output format 'text', falling back to json — but no JSON follows.

Case D: people people searchContacts

gws people people searchContacts --params "{\"query\":\"<term>\",\"readMask\":\"names,emailAddresses\"}"

Expected: JSON with search results matching known contacts.

Actual: Exit 0. Empty stdout.

Hypothesis

The failure mode looks like a swallowed serialization error. Evidence:

  1. The +read case emits unknown output format 'text', falling back to json to stderr before producing empty stdout — the output pipeline is entering a fallback path that then fails silently.
  2. The messages get case is ID-specific, not command-specific — which suggests sensitivity to response shape (e.g., specific multipart structures, header configurations, or MIME types present in one message but not another).
  3. The broken endpoints share a common feature: they return variable-shape payloads. labels list returns a heterogeneous list (system and user labels have different field sets). connections list and searchContacts return Person objects with optional fields whose presence depends on the contact. Meanwhile, the endpoints that work reliably return shapes that are stable per-call.

A reasonable guess: the output formatter panics or errors on certain response shapes and the error is caught and discarded (e.g., via .ok() or let _ = ...) rather than surfaced.

Impact

Critical for automation. Any script, skill, or agent that treats exit 0 + empty stdout as "no results" will:

  • Miss Gmail labels entirely on a fresh read
  • Fail to retrieve certain messages
  • Skip contact searches entirely
  • Produce wrong, not just slow, downstream behavior

Requested behavior

  1. Never emit empty stdout on a successful API call. Even for a genuinely empty result, return a structurally valid JSON envelope ({"items": []}, {"labels": []}, etc.) that downstream consumers can parse.
  2. Exit non-zero when output serialization fails. If the formatter cannot produce output for a response it received, that is an error condition — exit with a distinct non-zero code and print a descriptive message to stderr including the endpoint and relevant debug information.
  3. Consider a --raw or --debug-response flag that bypasses the formatter and prints the raw API response, so users can recover data even when the formatter fails on a specific payload shape.

Proposed fix direction

Audit the output-formatting pipeline for silently-caught errors. In Rust, this often looks like .ok() or let _ = ... on a Result where ? or .unwrap_or_else(|e| log_and_exit(e)) would be correct. A grep for .ok() on serialization or formatting results would be a reasonable starting point.

Integration tests that would catch this class of bug:

  • labels list against an account with at least one user label (assert: exit 0 AND stdout is non-empty AND parses as JSON AND .labels | length >= 1)
  • messages get against a message with a complex multipart structure
  • searchContacts with a query matching at least one known contact

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions