Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/changelog/fix-pre-publish-error-handling
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

The Bluesky pre-publish preview now shows a clear message when it can't be generated — including a distinct one for permission errors — instead of failing without explanation.
2 changes: 1 addition & 1 deletion build/pre-publish-panel/plugin.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => '8fa0d7dd1bccedfa1d37');
<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => '424b2fbff0951a70d6d3');
8 changes: 4 additions & 4 deletions build/pre-publish-panel/plugin.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions includes/rest/admin/class-pre-publish-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use function Atmosphere\debug_log;
use function Atmosphere\is_connected;
use function Atmosphere\is_supported_post_type;

Expand Down Expand Up @@ -228,6 +229,27 @@ public function get_preview( WP_REST_Request $request ) {

try {
$projection = ( new Post( $draft ) )->project();
} catch ( \Throwable $e ) {
/*
* `project()` runs the_content over raw, unsaved editor markup,
* so a malformed block or a misbehaving content/shortcode filter
* can throw. Return a structured error (and log the post ID for
* support) instead of letting the keystroke-driven endpoint fatal
* into an opaque 500.
*/
debug_log(
\sprintf(
'pre-publish projection failed for post %d: %s',
$draft->ID,
$e->getMessage()
)
);

return new \WP_Error(
'atmosphere_projection_failed',
\__( 'The Bluesky preview could not be generated.', 'atmosphere' ),
array( 'status' => 500 )
);
} finally {
\remove_filter( 'pre_http_request', $block_http, 0 );
}
Expand Down
10 changes: 10 additions & 0 deletions includes/rest/class-client-metadata-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Atmosphere\OAuth\Client;
use WP_REST_Response;
use WP_REST_Server;
use function Atmosphere\debug_log;
use function Atmosphere\sanitize_text;

/**
Expand Down Expand Up @@ -141,6 +142,15 @@ public function get_metadata(): WP_REST_Response {
\esc_html__( 'atmosphere_client_metadata must return an array with a non-empty string client_id and a redirect_uris list of admin URLs; falling back to the unfiltered metadata.', 'atmosphere' ),
'1.0.0'
);

/*
* `_doing_it_wrong()` is silent in production. Also route the
* failure through debug_log() so operators can opt into the
* signal via the `atmosphere_debug_log` filter without enabling
* WP_DEBUG site-wide — an OAuth client_id served from a
* misbehaving filter is worth surfacing.
*/
debug_log( 'atmosphere_client_metadata filter returned an invalid value; using the unfiltered metadata.' );
}

$response = new WP_REST_Response( $metadata, 200 );
Expand Down
44 changes: 39 additions & 5 deletions src/pre-publish-panel/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function PrePublishPanel() {

const [ preview, setPreview ] = useState( null );
const [ loading, setLoading ] = useState( true );
const [ error, setError ] = useState( false );
const [ error, setError ] = useState( null );
const debounce = useRef( null );

useEffect( () => {
Expand All @@ -54,7 +54,7 @@ function PrePublishPanel() {
}

setLoading( true );
setError( false );
setError( null );

// Debounce so each keystroke doesn't fire a projector request.
clearTimeout( debounce.current );
Expand All @@ -76,8 +76,16 @@ function PrePublishPanel() {
setPreview( result );
setLoading( false );
} )
.catch( () => {
setError( true );
.catch( ( err ) => {
// Keep the error so the message can distinguish a
// permission failure from a transient one, and log it so
// a support report has something to go on.
// eslint-disable-next-line no-console -- Aid debugging.
console.error(
'ATmosphere pre-publish preview failed:',
err
);
setError( err );
setLoading( false );
} );
}, 400 );
Expand All @@ -89,7 +97,33 @@ function PrePublishPanel() {
return <Spinner />;
}

if ( error || ! preview ) {
if ( error ) {
const errorCode = error?.code;
const errorStatus = error?.data?.status;
// An expired/invalid nonce is a 403 too, but it's transient (a
// reload fixes it), so it must not read as a permission failure.
const isAuth =
'rest_cookie_invalid_nonce' !== errorCode &&
( 'rest_forbidden' === errorCode ||
401 === errorStatus ||
403 === errorStatus );

return (
<p>
{ isAuth
? __(
'You don’t have permission to preview this post.',
'atmosphere'
)
: __(
'Could not load the Bluesky preview. Please try again.',
'atmosphere'
) }
</p>
);
}

if ( ! preview ) {
return (
<p>{ __( 'Could not load the Bluesky preview.', 'atmosphere' ) }</p>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,36 @@ public function test_preview_connected_short_form() {
$this->assertFalse( $data['records'][0]['over_limit'] );
}

/**
* A projection that throws (e.g. malformed block markup tripping a
* content filter) is caught and reported as a structured error, not an
* opaque fatal.
*
* @covers ::get_preview
*/
public function test_preview_returns_error_when_projection_throws() {
$post = self::factory()->post->create_and_get( array( 'post_title' => '' ) );

$thrower = static function () {
throw new \RuntimeException( 'boom' );
};
\add_filter( 'the_content', $thrower, 9 );

try {
$result = $this->controller->get_preview(
$this->make_request( $post->ID, array( 'content' => 'A quick note.' ) )
);
} finally {
// Remove in finally so a failed assertion below can't leak the
// throwing filter into later tests.
\remove_filter( 'the_content', $thrower, 9 );
}

$this->assertInstanceOf( \WP_Error::class, $result );
$this->assertSame( 'atmosphere_projection_failed', $result->get_error_code() );
$this->assertSame( 500, $result->get_error_data()['status'] );
}

/**
* A draft (not yet published) reports the real character count, not a
* redacted zero. The transformer redacts non-published posts, so the
Expand Down