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 nowView on GitHub (pinned to 01e15490f0)
Solutions
- Branch on the specific status code: 401 -> refresh token, 400 -> validate payload, 5xx -> retry with backoff.
- Validate content length and structure before posting (strip/shorten, count weighted characters).
- Refresh OAuth tokens proactively before expiry rather than waiting for a 401.
- For 5xx responses, retry with exponential backoff and jitter up to a bounded number of attempts.
- 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
- Validate content length (counting weighted characters) before posting.
- Refresh OAuth tokens before they expire.
- Branch on status code instead of one catch-all.
- Retry only 5xx with bounded backoff.
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
- Rate limited. Resets at {reset}
- Config file not found: {path}
- Forbidden: {resp.json().get('detail', 'check permissions')}
- Missing value for --target
- Unknown argument: ${arg}
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/4013d5aa63ad3972.
Report an issue: GitHub.