affaan-m/ECC · error · Exception

X API error {resp.status_code}: {resp.text}

Error message

X API error {resp.status_code}: {resp.text}

What it means

A catch-all generic Exception raised by X (Twitter) API posting code for any response status other than 201, 429, and 403. It includes the raw status code and response body. This branch exists to surface unexpected API responses rather than silently returning None.

Source

Thrown at skills/x-api/SKILL.md:208

if remaining < 5:
    reset = int(resp.headers.get("x-rate-limit-reset", 0))
    wait = max(0, reset - int(time.time()))
    print(f"Rate limit approaching. Resets in {wait}s")
```

## Error Handling

```python
resp = oauth.post("https://api.x.com/2/tweets", json={"text": content})
if resp.status_code == 201:
    return resp.json()["data"]["id"]
elif resp.status_code == 429:
    reset = int(resp.headers["x-rate-limit-reset"])
    raise Exception(f"Rate limited. Resets at {reset}")
elif resp.status_code == 403:
    raise Exception(f"Forbidden: {resp.json().get('detail', 'check permissions')}")
else:
    raise Exception(f"X API error {resp.status_code}: {resp.text}")
```

## Security

- **Never hardcode tokens.** Use environment variables or `.env` files.
- **Never commit `.env` files.** Add to `.gitignore`.
- **Rotate tokens** if exposed. Regenerate at developer.x.com.
- **Use read-only tokens** when write access is not needed.
- **Store OAuth secrets securely** — not in source code or logs.

## Integration with Content Engine

Use `brand-voice` plus `content-engine` to generate platform-native content, then post via X API:
1. Pull recent original posts when voice matching matters
2. Build or reuse a `VOICE PROFILE`
3. Generate content with `content-engine` in X-native format
4. Validate length and thread structure
5. Return the draft for approval unless the user explicitly asked to post now

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Branch on the specific status code: 401 -> refresh token, 400 -> validate payload, 5xx -> retry with backoff.
  2. Validate content length and structure before posting (strip/shorten, count weighted characters).
  3. Refresh OAuth tokens proactively before expiry rather than waiting for a 401.
  4. For 5xx responses, retry with exponential backoff and jitter up to a bounded number of attempts.
  5. Log resp.text in full server-side; do not surface raw X error text to end users as it may leak details.

Example fix

# before
else:
    raise Exception(f"X API error {resp.status_code}: {resp.text}")

# after: route by status code
if resp.status_code == 401:
    refresh_oauth_token()
    return retry_post(content)
if resp.status_code == 400:
    raise InvalidTweetError(resp.json().get('detail', 'bad request'))
if 500 <= resp.status_code < 600:
    raise TransientXError(f"X {resp.status_code}; retryable")
raise XAPIError(f"X {resp.status_code}: {resp.text}")
Defensive patterns

Strategy: try-catch

Validate before calling

def payload_is_postable(content: str) -> bool:
    return isinstance(content, str) and 0 < len(content) <= 280

Type guard

null

Try / catch

try:
    post_tweet(content)
except TransientXError:
    backoff_retry(post_tweet, content, attempts=3)
except InvalidTweetError as e:
    log.warning("rejected: %s", e)
except XAPIError as e:
    log.error("unexpected X error %s", e)
    raise

Prevention

When it happens

Trigger: Any non-success, non-429, non-403 response: 400 Bad Request (malformed body, text > 280 chars), 401 Unauthorized (bad/expired token), 409/422 for invalid media references, 500/502/503 for X-side outages, or unexpected new status codes from API changes.

Common situations: Tweet text exceeds the character limit after URL/tco wrapping; token expired and was not refreshed; media media_id was not yet available when referenced; X is having a service incident; API version changed and the client targets a removed endpoint.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/4013d5aa63ad3972. Report an issue: GitHub.