1. 記事一覧 >
  2. ブログ記事
category logo

Next.jsのfetchとstatic-tweetのfetchのプロキシ対応

(更新) (公開)

はじめに

このブログのソースコードnpm run dev で プロキシサーバー越しにmicroCMSとTwitterのAPIをGETできるようになっています。
microCMS APIの方は、fetch関数部分をプロキシ環境変数を使うように書き換えて対応しました。
Twitter APIの方は、npmのreact-static-tweets(GitHub)からGETされていて、自前のソースコードではなく、対応に困りました。

react-static-tweetsは、static-tweet(GitHub)を使ってます。

Next.js Proxyの図1

最終的に、Twitter APIの方は、patch-packageを使ってnode_modules/static-tweetsを書き換えて解決しました。
今回は、fetchのProxy対応を主題とし、patch-package主題の話と2回に分けて書きたいと思います。


組み込みのfetchの変更

Next.js Proxyの図2

以下のようにhttps-proxy-agentを使って、対処しました。
process.env.https_proxy(環境変数のhttps_proxy)が有ったら、
fetchの第二引数を

{
    headers: header,
    agent: new HttpsProxyAgent(プロキシサーバー),
}

のようにagent:キーを追加する感じです。
プロキシサーバーの部分は、http://proxyserver.example:3128のような文字列になります。
なお、process.env.https_proxyは、.env.local

https_proxy=http://proxyserver.example:3128

と書いてもOKです。
これが無い場合、OSの環境変数https_proxyが採用されます。


diff(一例です。):

pages/blogs/[id].tsx CHANGED
@@ -20,6 +20,7 @@ import useMobileDevice from '../../hooks/useMobileDevice';
20
20
  import MobileShare from '../../components/mobileShare';
21
21
  import tocbot from 'tocbot';
22
22
  import { OpenGraphImages } from 'next-seo/lib/types';
23
+ import { HttpsProxyAgent } from 'https-proxy-agent';
23
24
 
24
25
  interface Tweet {
25
26
  id: string;
@@ -420,8 +420,12 @@ export const getStaticProps: GetStaticProps<Props, Slug> = async ({
420
420
 
421
421
  const header: HeadersInit = new Headers();
422
422
  header.set('X-API-KEY', process.env.API_KEY || '');
423
- const key = {
423
+ const proxy = process.env.https_proxy;
424
+ const key = proxy ? {
424
425
  headers: header,
426
+ agent: new HttpsProxyAgent(proxy)
427
+ } : {
428
+ headers: header
425
429
  };
426
430
  const data: Content = await fetch(`${process.env.API_URL}contents/` + id, key)
427
431
  .then((res) => res.json())
@@ -454,8 +458,12 @@ export const getStaticProps: GetStaticProps<Props, Slug> = async ({
454
458
  export const getStaticPaths: GetStaticPaths<Slug> = async () => {
455
459
  const header: HeadersInit = new Headers();
456
460
  header.set('X-API-KEY', process.env.API_KEY || '');
457
- const key = {
461
+ const proxy = process.env.https_proxy;
462
+ const key = proxy ? {
458
463
  headers: header,
464
+ agent: new HttpsProxyAgent(proxy)
465
+ } : {
466
+ headers: header
459
467
  };
460
468
  const data: ContentRootObject = await fetch(
461
469
  `${process.env.API_URL}contents?limit=9999&fields=id`,

react-static-tweetsについて

ビルド時にツイッターのデータを取得して、HTMLとして生成しておけるものです。これにより、表示するときは、ただのHTMLですので、負荷を減らせます。

$ npm install react-static-tweets

でインストールして、

import { fetchTweetAst } from 'static-tweets';

getStaticProps内で、

  const tweets = await Promise.all(
    twitter_ids.map(async (id: string) => {
      const ast = await fetchTweetAst(id);
      return { id, ast };
    })
  );

のようにTweetIDを渡して、ツィートデータ(解析結果)を取得しています。

それをさらに、

import { Tweet } from 'react-static-tweets';
(略)
        {tweets.map((tweet) => {
          return <Tweet key={tweet.id} id={tweet.id} ast={tweet.ast} />;
        })}
(略)

で描画して、結果、以下のように表示されます。
react-static-tweetsの表示例


static-tweetsのfetchについて

プロキシ環境変数をセットしただけで、起動すると、Twitter APIへのGETでエラーになります。

$ export https_proxy="http://192.168.0.158:3128"
$ export HTTPS_PROXY="http://192.168.0.158:3128"
$ npm run dev

Next.js Proxyの図3


Twitter APIへのGETでエラー
ターミナルに以下のエラーが出力されました。

Error: queryA EREFUSED syndication.twitter.com
    at QueryReqWrap.onresolve [as oncomplete] (dns.js:203:19) {
  errno: undefined,
  code: 'EREFUSED',
  syscall: 'queryA',
  hostname: 'syndication.twitter.com',
  url: 'https://syndication.twitter.com/tweets.json?ids=1411580890468077575',
  opts: {
    agent: HttpsAgent {
      _events: [Object: null prototype],
      _eventsCount: 4,
      _maxListeners: undefined,
      defaultPort: 443,
      protocol: 'https:',
      options: [Object],
      requests: {},
      sockets: {},
      freeSockets: {},
      keepAliveMsecs: 1000,
      keepAlive: true,
      maxSockets: Infinity,
      maxFreeSockets: 256,
      freeSocketKeepAliveTimeout: 15000,
      timeout: 30000,
      socketActiveTTL: null,
      createSocketCount: 0,
      createSocketCountLastCheck: 0,
      createSocketErrorCount: 0,
      createSocketErrorCountLastCheck: 0,
      closeSocketCount: 0,
      closeSocketCountLastCheck: 0,
      errorSocketCount: 0,
      errorSocketCountLastCheck: 0,
      requestCount: 0,
      requestCountLastCheck: 0,
      timeoutSocketCount: 0,
      timeoutSocketCountLastCheck: 0,
      maxCachedSessions: 100,
      _sessionCache: [Object],
      [Symbol(kCapture)]: false
    },
    redirect: 'manual',
    headers: Headers { [Symbol(map)]: [Object: null prototype] },
    onRedirect: [Function (anonymous)]
  }
}

react-static-tweets

packages/static-tweets/src/fetchTweetAst.tsから

export async function fetchTweetAst(tweetId: string): Promise<any> {
  const tweetHtml = await fetchTweetHtml(tweetId)

packages/static-tweets/src/twitter/api.ts

export async function fetchTweetsHtml(ids) {
  const res = await fetch(`${SYNDICATION_URL}/tweets.json?ids=${ids}`)

fetchでエラーになっていました。


このfetchが何者かと言うと、

packages/static-tweets/src/twitter/api.ts

import fetch from '../fetch'

で取り込まれていて、

packages/static-tweets/src/fetch.ts

import vercelFetch from '@vercel/fetch'

export default vercelFetch()

となっていますので、@vercel/fetch でした。


そして、https-proxy-agentを使えば良いのかなと思いましたが、Next.jsのfetch@vercel/fetchとは別物で、同じ手法は使えませんでした。

getStaticProps内のfetch: [Function: fetchWithAgent]
fetchTweetsHtmlfetch: [AsyncFunction: vercelFetch]

Next.jsのソースコード(GitHub)fetchWithAgentを見ると、

packages/next/server/node-polyfill-fetch.js
import fetch, { Headers, Request, Response } from 'node-fetch'

// Polyfill fetch() in the Node.js environment
if (!global.fetch) {
  const agent = ({ protocol }) =>
    protocol === 'http:' ? global.__NEXT_HTTP_AGENT : global.__NEXT_HTTPS_AGENT
  const fetchWithAgent = (url, opts, ...rest) => {
    if (!opts) {
      opts = { agent }
    } else if (!opts.agent) {
      opts.agent = agent
    }
    return fetch(url, opts, ...rest)
  }
  global.fetch = fetchWithAgent
  global.Headers = Headers
  global.Request = Request
  global.Response = Response
}

となっていて、node-fetchを使っています。


static-tweets変更

Next.js Proxy static-tweetsのfetchでエラー

packages/static-tweets/src/twitter/api.ts

import fetch from '../fetch'

をコメントアウトして、agentを受け取るisomorphic-fetchを使うことにしました。

サーバーサイド(ビルド時)でしかfetchしていないはずですので、node-fetchで良かったかもしれません。実際特に問題無かったです。


diff:

node_modules/static-tweets/src/twitter/api.ts CHANGED
@@ -1,4 +1,6 @@
1
- import fetch from '../fetch'
1
+ //import fetch from '../fetch'
2
+ import fetch from 'isomorphic-fetch';
3
+ import { HttpsProxyAgent } from 'https-proxy-agent';
2
4
 
3
5
  const API_URL = 'https://api.twitter.com'
4
6
  const SYNDICATION_URL = 'https://syndication.twitter.com'
@@ -13,7 +15,8 @@ function twitterLabsEnabled(expansions) {
13
15
  }
14
16
 
15
17
  export async function fetchTweetsHtml(ids) {
16
- const res = await fetch(`${SYNDICATION_URL}/tweets.json?ids=${ids}`)
18
+ const proxy = process.env.https_proxy;
19
+ const res = await fetch(`${SYNDICATION_URL}/tweets.json?ids=${ids}`, proxy ? { agent: new HttpsProxyAgent(proxy) } : {})
17
20
 
18
21
  if (res.ok) return res.json()
19
22
  if (res.status === 404) return {}
@@ -32,14 +35,17 @@ export async function fetchUserStatus(tweetId) {
32
35
  // If there isn't an API token don't do anything, this is only required for videos.
33
36
  if (!process.env.TWITTER_ACCESS_TOKEN) return
34
37
 
35
- const res = await fetch(
36
- `${API_URL}/1.1/statuses/show/${tweetId}.json?include_entities=true&tweet_mode=extended`,
37
- {
38
- headers: {
39
- authorization: `Bearer ${process.env.TWITTER_ACCESS_TOKEN}`
40
- }
38
+ const proxy = process.env.https_proxy;
39
+ const res = await fetch(`${API_URL}/1.1/statuses/show/${tweetId}.json?include_entities=true&tweet_mode=extended`, proxy ? {
40
+ headers: {
41
+ authorization: `Bearer ${process.env.TWITTER_ACCESS_TOKEN}`
42
+ },
43
+ agent: new HttpsProxyAgent(proxy)
44
+ } : {
45
+ headers: {
46
+ authorization: `Bearer ${process.env.TWITTER_ACCESS_TOKEN}`
41
47
  }
42
- )
48
+ })
43
49
 
44
50
  console.log(
45
51
  'Twitter x-rate-limit-limit:',
@@ -68,14 +74,18 @@ export async function fetchTweetWithPoll(tweetId) {
68
74
  if (!process.env.TWITTER_ACCESS_TOKEN || !twitterLabsEnabled(expansions))
69
75
  return
70
76
 
71
- const res = await fetch(
72
- `${API_URL}/labs/1/tweets?format=compact&expansions=${expansions}&ids=${tweetId}`,
73
- {
74
- headers: {
75
- authorization: `Bearer ${process.env.TWITTER_ACCESS_TOKEN}`
76
- }
77
+ const proxy = process.env.https_proxy;
78
+ const res = await fetch(`${API_URL}/labs/1/tweets?format=compact&expansions=${expansions}&ids=${tweetId}`, proxy ? {
79
+ headers: {
80
+ authorization: `Bearer ${process.env.TWITTER_ACCESS_TOKEN}`
81
+ },
82
+ agent: new HttpsProxyAgent(proxy)
83
+ } : {
84
+ headers: {
85
+ authorization: `Bearer ${process.env.TWITTER_ACCESS_TOKEN}`
77
86
  }
78
- )
87
+ })
88
+
79
89
 
80
90
  console.log(
81
91
  'Twitter Labs x-rate-limit-limit:',
@@ -99,7 +109,8 @@ export async function fetchTweetWithPoll(tweetId) {
99
109
  }
100
110
 
101
111
  export async function getEmbeddedTweetHtml(url) {
102
- const res = await fetch(`https://publish.twitter.com/oembed?url=${url}`)
112
+ const proxy = process.env.https_proxy;
113
+ const res = await fetch(`https://publish.twitter.com/oembed?url=${url}`, proxy ? { agent: new HttpsProxyAgent(proxy) } : {})
103
114
 
104
115
  if (res.ok) return res.json()
105
116
  if (res.status === 404) return

ここまでで対応方法は分かったのですが・・・
react-static-tweetsをforkするのか、自分のソースコード(この場合、itc-blog)に取り込んでしまって良いのか?と対応に迷いが生じ、パッチを当てる方法を見つけて、実践しました。
続きは、npmのpatch-packageの話になります。

npmのpatch-packageを使って、static-tweetにパッチを当てた

loading...