Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gatsby, gatsby-plugin-utils, gatsby-source-wordpress, gatsby-source-contentful, gatsby-source-drupal): Add setRequestHeaders action/api #35655

Merged
merged 59 commits into from
Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
50b4491
start adding new action
TylerBarnes May 12, 2022
1b88fc4
Add request headers api
TylerBarnes May 13, 2022
8e082b4
types are in src
TylerBarnes May 13, 2022
6057597
Add source plugin docs
TylerBarnes May 13, 2022
dc054d4
try removing type imports
TylerBarnes May 13, 2022
74f76c9
any
TylerBarnes May 13, 2022
aead073
move validation from reducer into setRequestHeaders action
TylerBarnes May 13, 2022
b49a3a2
add comments
TylerBarnes May 13, 2022
885031c
move headers into each place fetchRemoteFile is called instead of ins…
TylerBarnes May 13, 2022
d19dccd
for cases where gatsby core doesn't have this yet
TylerBarnes May 17, 2022
44141cf
add tests
TylerBarnes May 17, 2022
14b6730
use import from to get the gatsby store
TylerBarnes May 17, 2022
081cd4c
try any
TylerBarnes May 17, 2022
938033b
Add action type to action union
TylerBarnes May 17, 2022
1f57bb7
Update index.d.ts
TylerBarnes May 17, 2022
3408213
move test to the bottom so the state doesn't effect the other tests
TylerBarnes May 17, 2022
4ba7177
move getRequestHeadersForUrl into gatsby/utils
TylerBarnes May 17, 2022
c744b85
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 17, 2022
ca8fc21
it seems some test generates these
TylerBarnes May 17, 2022
cc58029
remove snapshots and expect size of request headers Map
TylerBarnes May 18, 2022
7faf0d9
Revert "move getRequestHeadersForUrl into gatsby/utils"
TylerBarnes May 18, 2022
c925bd5
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 18, 2022
c07f1d7
Update get-request-headers-for-url.ts
TylerBarnes May 18, 2022
a526a9e
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 19, 2022
801a85d
review changes
TylerBarnes May 19, 2022
dd9f607
pass store down instead of importing it
TylerBarnes May 19, 2022
bba3863
remove console log
TylerBarnes May 19, 2022
1f1c122
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 19, 2022
e9b12d5
add missing actions
TylerBarnes May 19, 2022
0f0588a
add store to addRemoteFilePolyfillInterface
TylerBarnes May 19, 2022
7115053
Merge branch 'feat/get-request-headers' of https://github.com/gatsbyj…
TylerBarnes May 19, 2022
0b82135
add httpHeaders to FILE_CDN job
TylerBarnes May 19, 2022
43714c0
unused import
TylerBarnes May 19, 2022
874d424
more missing stores and pass httpHeaders into job & transformImage in…
TylerBarnes May 19, 2022
497adf7
remove reporter import
TylerBarnes May 19, 2022
dff417e
Update get-request-headers-for-url.ts
TylerBarnes May 19, 2022
c2a5dfb
fix tests
TylerBarnes May 19, 2022
6634607
fix more tests
TylerBarnes May 19, 2022
bf48099
update warning message
TylerBarnes May 19, 2022
57c75d0
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 19, 2022
a519fd4
fix store type as it may not exist
TylerBarnes May 20, 2022
69ba76c
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 20, 2022
4bdda47
pass the actual request url, not the url url param
TylerBarnes May 20, 2022
fe9e2d0
Update placeholder-handler.ts
TylerBarnes May 20, 2022
a65242b
Merge branch 'feat/get-request-headers' of https://github.com/gatsbyj…
TylerBarnes May 20, 2022
4ac2b81
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 26, 2022
e3d7aad
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 26, 2022
96138fc
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 27, 2022
9bee6fd
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 27, 2022
39ee715
show test paths on output
TylerBarnes May 27, 2022
b7056e2
debugging
TylerBarnes May 28, 2022
a2b6d47
debugging
TylerBarnes May 28, 2022
90e4fe7
add missing btoa fn
TylerBarnes May 28, 2022
7899e8b
Merge branch 'master' into feat/get-request-headers
wardpeet May 30, 2022
c9d9884
don't use btoa, use a buffer instead
TylerBarnes May 30, 2022
b42308c
group imports
TylerBarnes May 30, 2022
f9745f4
remove store & actions requires
TylerBarnes May 30, 2022
56f6509
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 30, 2022
b7305b4
Merge branch 'master' into feat/get-request-headers
TylerBarnes May 31, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/docs/how-to/plugins-and-themes/creating-a-source-plugin.md
Expand Up @@ -1061,6 +1061,27 @@ You might notice that `width`, `height`, `resize`, and `gatsbyImage` can be null

The string returned from `gatsbyImage` is intended to work seamlessly with [Gatsby Image Component](/docs/reference/built-in-components/gatsby-plugin-image/#gatsbyimage) just like `gatsbyImageData` does.

#### Adding Image CDN request headers with the `setRequestHeaders` action

Since Gatsby will be fetching files from your CMS instead of your source plugin fetching those files, you may need to set request headers for Gatsby to use in those requests.
This is needed if for example your CMS is locked down behind some kind of authentication.
For each domain Image CDN will make requests to, set the required headers following this example:

```js
exports.onPluginInit = ({ actions }, pluginOptions) => {
if (typeof actions.setRequestHeaders === `function`) {
actions.setRequestHeaders({
// set the domain the headers should apply to
domain: pluginOptions.apiUrl,
headers: {
// add any needed headers
Authorization: pluginOptions.authToken,
},
})
}
}
```

#### `sourceNodes` node API additions

When creating nodes, you must add some fields to the node itself to match what the `RemoteFile` interface expects. You will need `url`, `mimeType`, `filename` as mandatory fields. When you have an image type, `width` and `height` are required as well. The optional fields are `placeholderUrl` and `filesize`. `placeholderUrl` will be the url used to generate blurred or dominant color placeholder so it should contain `%width%` and `%height%` url params if possible.
Expand Down
1 change: 1 addition & 0 deletions packages/gatsby-core-utils/src/fetch-remote-file.ts
Expand Up @@ -200,6 +200,7 @@ async function fetchFile({
// See if there's response headers for this url
// from a previous request.
const headers = { ...httpHeaders }

if (cachedEntry?.headers?.etag && (await fs.pathExists(filename))) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
headers[`If-None-Match`] = cachedEntry.headers.etag
Expand Down
Expand Up @@ -637,6 +637,54 @@ describe(`gatsbyImageData`, () => {
expect(parsedFullWidthSrcSet[1].descriptor).toEqual(`700w`)
})

it(`should fetch placeholder file with headers from the setRequestHeaders action`, async () => {
fetchRemoteFile.mockImplementationOnce(input => {
if (
!input.httpHeaders ||
input.httpHeaders.Authorization !== `Bearer 123`
) {
throw Error(`No headers found for url ${input.url}`)
} else {
return path.join(__dirname, `__fixtures__`, `dog-portrait.jpg`)
}
})

const { store } = jest.requireActual(`gatsby/dist/redux`)
const { actions } = jest.requireActual(`gatsby/dist/redux/actions/public`)

store.dispatch(
actions.setRequestHeaders(
{
domain: portraitSource.url,
headers: {
Authorization: `Bearer 123`,
},
},
{
name: `gatsby-source-test`,
}
)
)

const fixedResult = await gatsbyImageResolver(
portraitSource,
{
layout: `fixed`,
width: 300,
placeholder: PlaceholderType.BLURRED,
},
actions
)

expect(fetchRemoteFile).toHaveBeenCalledTimes(1)
expect(fetchRemoteFile).toHaveBeenCalledWith(
expect.objectContaining({
url: portraitSource.url,
})
)
expect(fixedResult?.placeholder).toBeTruthy()
})

it(`should generate dominant color placeholder by default`, async () => {
fetchRemoteFile.mockResolvedValueOnce(
path.join(__dirname, `__fixtures__`, `dog-portrait.jpg`)
Expand Down
Expand Up @@ -5,6 +5,7 @@ import { hasFeature } from "../has-feature"
import { ImageCDNUrlKeys } from "./utils/url-generator"
import { getFileExtensionFromMimeType } from "./utils/mime-type-helpers"
import { transformImage } from "./transform-images"
import { getRequestHeadersForUrl } from "./utils/get-request-headers-for-url"

import type { Application } from "express"

Expand All @@ -25,11 +26,15 @@ export function addImageRoutes(app: Application): Application {
`file`
)

const url = req.query[ImageCDNUrlKeys.URL] as string

const filePath = await fetchRemoteFile({
directory: outputDir,
url: req.query[ImageCDNUrlKeys.URL] as string,
url,
name: req.params.filename,
httpHeaders: getRequestHeadersForUrl(url),
})

fs.createReadStream(filePath).pipe(res)
})

Expand Down
Expand Up @@ -3,6 +3,7 @@ import { fetchRemoteFile } from "gatsby-core-utils/fetch-remote-file"
import { cpuCoreCount } from "gatsby-core-utils/cpu-core-count"
import Queue from "fastq"
import { transformImage } from "../transform-images"
import { getRequestHeadersForUrl } from "../utils/get-request-headers-for-url"

interface IImageServiceProps {
outputDir: Parameters<typeof transformImage>[0]["outputDir"]
Expand Down Expand Up @@ -35,11 +36,12 @@ export async function FILE_CDN({

await fetchRemoteFile({
directory: outputDir,
url: url,
url,
name: path.basename(filename, ext),
ext,
cacheKey: contentDigest,
excludeDigest: true,
httpHeaders: getRequestHeadersForUrl(url),
})
}

Expand Down
Expand Up @@ -7,6 +7,7 @@ import getSharpInstance from "gatsby-sharp"
import { getCache } from "./utils/cache"
import { getImageFormatFromMimeType } from "./utils/mime-type-helpers"
import type { IRemoteImageNode } from "./types"
import { getRequestHeadersForUrl } from "./utils/get-request-headers-for-url"

export enum PlaceholderType {
BLURRED = `blurred`,
Expand Down Expand Up @@ -57,6 +58,7 @@ const queue = Queue<
url,
cacheKey: contentDigest,
directory: tmpDir,
httpHeaders: getRequestHeadersForUrl(url),
})

switch (type) {
Expand Down
Expand Up @@ -4,6 +4,7 @@ import { fetchRemoteFile } from "gatsby-core-utils/fetch-remote-file"
import { createContentDigest } from "gatsby-core-utils/create-content-digest"
import getSharpInstance from "gatsby-sharp"
import { getCache } from "./utils/cache"
import { getRequestHeadersForUrl } from "./utils/get-request-headers-for-url"

export interface IResizeArgs {
width: number
Expand Down Expand Up @@ -48,10 +49,11 @@ export async function transformImage({
const basename = path.basename(filename, ext)
const filePath = await fetchRemoteFile({
directory: cache.directory,
url: url,
url,
name: basename,
ext,
cacheKey: contentDigest,
httpHeaders: getRequestHeadersForUrl(url),
})

const outputPath = path.join(outputDir, filename)
Expand Down
@@ -0,0 +1,27 @@
import url from "url"
import path from "path"
import importFrom from "import-from"
import resolveFrom from "resolve-from"

import type { Headers } from "got"

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getStore(): any {
const gatsbyPkgRoot = path.dirname(
resolveFrom(process.cwd(), `gatsby/package.json`)
)
TylerBarnes marked this conversation as resolved.
Show resolved Hide resolved

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { store } = importFrom(gatsbyPkgRoot, `gatsby/dist/redux`) as any
TylerBarnes marked this conversation as resolved.
Show resolved Hide resolved

return store
}

export function getRequestHeadersForUrl(
passedUrl: string
): Headers | undefined {
const baseDomain = url.parse(passedUrl).hostname
const { requestHeaders } = getStore().getState()

return baseDomain ? requestHeaders?.get(baseDomain) : undefined
}
1 change: 1 addition & 0 deletions packages/gatsby-source-wordpress/src/gatsby-node.ts
Expand Up @@ -7,6 +7,7 @@ module.exports = runApisInSteps({
steps.setGatsbyApiToState,
steps.setErrorMap,
steps.tempPreventMultipleInstances,
steps.setRequestHeaders,
],

pluginOptionsSchema: steps.pluginOptionsSchema,
Expand Down
2 changes: 2 additions & 0 deletions packages/gatsby-source-wordpress/src/steps/index.ts
Expand Up @@ -20,3 +20,5 @@ export {
export { pluginOptionsSchema } from "~/steps/declare-plugin-options-schema"
export { logPostBuildWarnings } from "~/steps/log-post-build-warnings"
export { imageRoutes } from "~/steps/image-routes"

export { setRequestHeaders } from "./set-request-headers"
25 changes: 25 additions & 0 deletions packages/gatsby-source-wordpress/src/steps/set-request-headers.ts
@@ -0,0 +1,25 @@
import btoa from "btoa"
TylerBarnes marked this conversation as resolved.
Show resolved Hide resolved

import { getPluginOptions } from "~/utils/get-gatsby-api"

import type { Step } from "~/utils/run-steps"

export const setRequestHeaders: Step = ({ actions }): void => {
if (typeof actions?.setRequestHeaders !== `function`) {
return
}

const pluginOptions = getPluginOptions()

const { auth, url } = pluginOptions
const { password, username } = auth?.htaccess || {}

if (password && username) {
actions.setRequestHeaders({
domain: url,
headers: {
Authorization: `Basic ${btoa(`${username}:${password}`)}`,
},
})
}
}
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Add request headers allows you to add request headers 1`] = `
Object {
"payload": Object {
"domain": "testdomain.com",
"headers": Object {
"X-Header": "test",
},
},
"type": "SET_REQUEST_HEADERS",
}
`;

exports[`Add request headers allows you to add request headers 2`] = `
Map {
"testdomain.com" => Object {
"X-Header": "test",
},
}
`;
68 changes: 68 additions & 0 deletions packages/gatsby/src/redux/__tests__/set-request-headers.ts
@@ -0,0 +1,68 @@
import { setRequestHeadersReducer as reducer } from "../reducers/set-request-headers"
import { actions } from "../actions"
import * as reporter from "gatsby-cli/lib/reporter"

jest.mock(`gatsby-cli/lib/reporter`, () => {
return {
__esModule: true,
warn: jest.fn(),
}
})

const testPlugin = {
name: `gatsby-source-test`,
}

describe(`Add request headers`, () => {
it(`allows you to add request headers`, () => {
const action = actions.setRequestHeaders(
{
domain: `https://testdomain.com/subpath`,
headers: {
"X-Header": `test`,
},
},
testPlugin
)
expect(action.payload.domain).toBe(`testdomain.com`)
expect(action.payload.headers).toEqual({
"X-Header": `test`,
})
expect(action).toMatchSnapshot()
TylerBarnes marked this conversation as resolved.
Show resolved Hide resolved

const state = reducer(undefined, action)

expect(state.get(`testdomain.com`)).toEqual({
"X-Header": `test`,
})
expect(state).toMatchSnapshot()
})

it(`fails if domain is missing`, () => {
actions.setRequestHeaders(
{
headers: {
"X-Header": `test`,
},
},
testPlugin
)

expect(reporter.warn).toHaveBeenCalledWith(
`Plugin gatsby-source-test called actions.setRequestHeaders with a domain property that isn't a string.`
)
})

it(`fails if headers are missing`, () => {
actions.setRequestHeaders(
{
domain: `https://testdomain.com/subpath`,
},
testPlugin
)

expect(reporter.warn).toHaveBeenCalledWith(
`Plugin gatsby-source-test called actions.setRequestHeaders with a headers property that isn't an object.`
)
})
})
47 changes: 47 additions & 0 deletions packages/gatsby/src/redux/actions/public.js
@@ -1,4 +1,5 @@
// @flow
const reporter = require(`gatsby-cli/lib/reporter`)
const chalk = require(`chalk`)
const _ = require(`lodash`)
const { stripIndent } = require(`common-tags`)
Expand Down Expand Up @@ -1478,4 +1479,50 @@ actions.unstable_createNodeManifest = (
}
}

/**
* Stores request headers for a given domain to be later used when making requests for Image CDN (and potentially other features).
*
* @param {Object} $0
* @param {string} $0.domain The domain to store the headers for.
* @param {Object} $0.headers The headers to store.
*/
actions.setRequestHeaders = ({ domain, headers }, plugin: Plugin) => {
TylerBarnes marked this conversation as resolved.
Show resolved Hide resolved
const noHeaders = typeof headers !== `object`
TylerBarnes marked this conversation as resolved.
Show resolved Hide resolved
const noDomain = typeof domain !== `string`

if (noHeaders) {
reporter.warn(
TylerBarnes marked this conversation as resolved.
Show resolved Hide resolved
`Plugin ${plugin.name} called actions.setRequestHeaders with a headers property that isn't an object.`
)
}

if (noDomain) {
reporter.warn(
`Plugin ${plugin.name} called actions.setRequestHeaders with a domain property that isn't a string.`
)
}

if (noDomain || noHeaders) {
return null
}

const baseDomain = url.parse(domain)?.hostname

if (baseDomain) {
return {
type: `SET_REQUEST_HEADERS`,
payload: {
domain: baseDomain,
headers,
},
}
} else {
reporter.warn(
`Plugin ${plugin.name} called actions.setRequestHeaders with a domain that is not a valid URL. (${domain})`
)

return null
}
}

module.exports = { actions }