diff --git a/ghost/core/core/server/services/oembed/TwitterOEmbedProvider.js b/ghost/core/core/server/services/oembed/TwitterOEmbedProvider.js index f280f9d538..78cf0750b2 100644 --- a/ghost/core/core/server/services/oembed/TwitterOEmbedProvider.js +++ b/ghost/core/core/server/services/oembed/TwitterOEmbedProvider.js @@ -71,6 +71,14 @@ class TwitterOEmbedProvider { oembedData.tweet_data = body.data; oembedData.tweet_data.includes = body.includes; } catch (err) { + if (err.response?.body) { + try { + const parsed = JSON.parse(err.response.body); + err.context = parsed; + } catch (e) { + err.context = err.response.body; + } + } logging.error(err); } } diff --git a/ghost/core/test/unit/server/services/oembed/twitter-embed.test.js b/ghost/core/test/unit/server/services/oembed/twitter-embed.test.js index 23fdab31c4..a6b4969259 100644 --- a/ghost/core/test/unit/server/services/oembed/twitter-embed.test.js +++ b/ghost/core/test/unit/server/services/oembed/twitter-embed.test.js @@ -1,8 +1,11 @@ const assert = require('assert/strict'); +const logging = require('@tryghost/logging'); +const sinon = require('sinon'); const TwitterOEmbedProvider = require('../../../../../core/server/services/oembed/TwitterOEmbedProvider'); const externalRequest = require('../../../../../core/server/lib/request-external'); const nock = require('nock'); const {mockManager} = require('../../../../utils/e2e-framework'); +const {HTTPError} = require('got'); describe('TwitterOEmbedProvider', function () { before(async function () { @@ -18,6 +21,27 @@ describe('TwitterOEmbedProvider', function () { mockManager.restore(); }); + function nockOembedRequest() { + nock('https://publish.twitter.com') + .get('/oembed') + .query(true) + .reply(200, { + data: { + conversation_id: '1630581157568839683', + public_metrics: { + retweet_count: 6, + reply_count: 1, + like_count: 27 + } + }, + includes: { + verified: false, + description: 'some description', + location: 'someplace, somewhere' + } + }); + } + it('Can support requests for Twitter URLs', async function () { const provider = new TwitterOEmbedProvider(); const tweetURL = new URL( @@ -48,26 +72,10 @@ describe('TwitterOEmbedProvider', function () { 'https://twitter.com/Ghost/status/1630581157568839683' ); - // not certain why we hit publish.twitter.com - nock('https://publish.twitter.com') - .get('/oembed') - .query(true) - .reply(200, { - data: { - conversation_id: '1630581157568839683', - public_metrics: { - retweet_count: 6, - reply_count: 1, - like_count: 27 - } - }, - includes: { - verified: false, - description: 'some description', - location: 'someplace, somewhere' - } - }); + // oembed-extractor first fetches the main oembed data + nockOembedRequest(); + // then we fetch additional data from the Twitter API nock('https://api.twitter.com') .get('/2/tweets/1630581157568839683') .query(true) @@ -93,4 +101,41 @@ describe('TwitterOEmbedProvider', function () { assert.ok(oembedData.data); assert.ok(oembedData.includes); }); + + it('logs error with context when external request fails', async function () { + const provider = new TwitterOEmbedProvider({ + config: { + bearerToken: 'test' + } + }); + const tweetURL = new URL( + 'https://twitter.com/Ghost/status/1630581157568839683' + ); + + nockOembedRequest(); + + nock('https://api.twitter.com') + .get('/2/tweets/1630581157568839683') + .query(true) + .reply(403, { + client_id: '12345', + detail: 'Testing requests are forbidden!', + reason: 'thou-shalt-not-pass' + }); + + const loggingStub = sinon.stub(logging, 'error'); + + await provider.getOEmbedData(tweetURL, externalRequest); + + sinon.assert.calledOnce(loggingStub); + sinon.assert.calledWithMatch(loggingStub, + sinon.match.instanceOf(HTTPError).and( + sinon.match.has('context', { + client_id: '12345', + detail: 'Testing requests are forbidden!', + reason: 'thou-shalt-not-pass' + }) + ) + ); + }); });