fix(facebook): use upload_url + file_url for reel transfer phase#32
Merged
Conversation
Production was failing every Facebook reel publish with the cryptic
'Video Upload Is Missing' / error_subcode 1363130. Root cause: our
'transfer' phase was making up its own API contract that doesn't exist.
The Reels publishing API (per Meta docs) is a 3-step flow:
1. start → POST /{page_id}/video_reels {upload_phase=start}
returns {video_id, upload_url}
2. transfer → POST {upload_url} on rupload.facebook.com
Header: Authorization: OAuth {token}
Body (JSON): {"file_url": "https://..."}
3. finish → POST /{page_id}/video_reels {upload_phase=finish, ...}
Our code was doing:
2 (broken) → POST /{video_id} on graph endpoint
Body: {video_file_chunk: '<some URL>', access_token: ...}
`video_file_chunk` is not a real parameter; the graph endpoint accepted
the request (returning 200) but nothing was actually uploaded, so the
finish phase reported 'Video was not uploaded'.
Changes:
- Capture upload_url from the start response and use it for transfer.
- POST to that upload_url with file_url in JSON body and
'Authorization: OAuth ...' header (the format docs.facebook.com
documents for the hosted-file flow).
- Bail with handleApiError if the start response is missing video_id
or upload_url so we don't silently no-op like before.
Tests:
- Existing 'can publish reel' test updated to include upload_url in the
start mock and assert the transfer call actually POSTs to rupload
with file_url + OAuth header (would have caught this bug before
shipping).
- New 'fails reel publish when start does not return upload_url' test
for the safety bail-out.
- Pre-existing 'cleans up temp files after reel upload' test updated
to match the new mock pattern.
Full suite green: 1504 passed, 2 skipped, 0 failed.
…_size headers
Previous attempt at the hosted-file flow (`file_url` in JSON body) hit
the rupload.facebook.com validator with HTTP 400:
'HeaderValuePredicate: Header Offset not convertable to unsigned long'
Facebook's rupload endpoint requires the `Offset` and `file_size` headers
regardless of whether the upload is local or hosted-file. The docs
describe the hosted-file path without them, but in practice rupload
rejects requests that lack them.
Switching to the well-documented local-file flow:
- Download the media to a temp file via Http sink (already a pattern
used in XPublisher for media downloads).
- POST raw bytes to upload_url with three headers:
- Authorization: OAuth {token}
- Offset: 0
- file_size: {actual bytes}
- Use mime_type from the media item as the Content-Type.
- Always cleanup the temp file in a finally block.
Tests updated to fake the media-download GET (returning bytes that the
publisher then re-uploads) and to assert on the Offset/file_size/
Authorization headers being present and correct. The existing
"cleans up temp files after reel upload" test now actually exercises
its assertion since this code path creates temp files again.
Full suite: 1504 passed, 2 skipped, 0 failed.
Five small fixes scoped to the publishReel method: 1. Replace generic \Exception on media download failure with a typed FacebookPublishException(ServerError). The generic exception was landing in the \Throwable catch in PublishToSocialPlatform with category 'unknown', defeating the whole point of the social exception hierarchy. 2. Replace handleApiError($startResponse) with a direct FacebookPublishException throw when video_id/upload_url are missing. The previous code passed a successful HTTP response into a method built for error responses — fromApiResponse would fall into the default arm and surface 'An unknown Facebook error occurred.' ironically reintroducing the same bad UX we just spent the day fixing. 3. Drop the four redundant Log::error calls before handleApiError. FacebookPublishException::fromApiResponse already pulls the FB error code/subcode/message into platformErrorCode + userMessage, and the downstream catch in PublishToSocialPlatform::handle logs the exception anyway (Nightwatch picks that up). Same pattern as the X cleanup in PR #29. 4. Stream the upload body via fopen() resource instead of file_get_contents(). Eliminates loading the whole video into memory for large reels. 5. Replace \@Unlink with unlink + Log::warning. Surfaces temp-file cleanup failures instead of silently leaking files. Tests: - Strengthened the missing-upload_url test to assert the exception message and class. - Added a typed-exception test for the media-download failure path (would have caught the regression where we used a generic \Exception). - Full suite green: 1505 passed, 2 skipped, 0 failed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Every Facebook reel publish was failing in production. The first attempt at a fix (commit `c379c4c` — hosted-file flow) failed in local testing too, this time with:
```
production.ERROR: Facebook reel upload transfer failed
{
"debug_info": {
"type": "ParameterValidationError",
"message": "HeaderValuePredicate: Header Offset not convertable to unsigned long"
}
}
```
So rupload.facebook.com requires the `Offset` and `file_size` headers even when using the hosted-file (`file_url`) path that Meta's docs document without them. To work around the docs/behavior gap, this PR switches to the well-documented local-file flow.
Root cause (full picture)
The Reels publishing API is a 3-step flow. We had step 2 implemented twice now but both wrong:
What this PR does
Tests