Skip to content

Instantly share code, notes, and snippets.

@ViieeS
Last active August 26, 2022 12:09
Show Gist options
  • Save ViieeS/139057f70bf98b66295b73bc3b30c391 to your computer and use it in GitHub Desktop.
Save ViieeS/139057f70bf98b66295b73bc3b30c391 to your computer and use it in GitHub Desktop.
NJS Web API

NJS Web API

npm install headers-polyfill
# or
yarn add headers-polyfill
import convertNjsRequestToCloudflare from './request-adaptor'
import RequestPolyfill from './request-polyfill'
describe('request-adaptor', () => {
it('convertNjsRequestToCloudflare function', () => {
const cloudflareRequest: RequestPolyfill = convertNjsRequestToCloudflare({
variables: { scheme: 'https' },
headersIn: {
Host: 'example.com',
foo: 'bar',
},
uri: '/path/name',
args: { foo: 'bar' },
} as unknown as NginxHTTPRequest)
expect(cloudflareRequest.url).toBe('https://example.com/path/name?foo=bar')
expect(cloudflareRequest.headers.get('Host')).toBe('example.com')
expect(cloudflareRequest.headers.get('foo')).toBe('bar')
})
})
import { Headers } from 'headers-polyfill'
import RequestPolyfill from './request-polyfill'
import resolveOrigin from './resolveOrigin'
/**
* This function converts NJS request to Cloudflare Request.
* @param request - NJS request
* @returns CloudFlare request
*/
export default function convertNjsRequestToCloudflare(
request: NginxHTTPRequest
): RequestPolyfill {
const scheme: string = request.variables.scheme || 'http'
const host = resolveOrigin(request.headersIn['Host'])
if (!host) {
throw new Error('Host header is empty.')
}
const urlSearchParams = new URLSearchParams(request.args)
// 1. Cloudflare API expects this field named as "url".
// 2. NJS request.uri never contains scheme & host, so we add both.
let urlString = `${scheme}://${host}${request.uri}`
if (urlSearchParams.toString()) {
urlString = urlString.concat('?', urlSearchParams.toString())
}
const url = new URL(urlString)
// HTTP headers
const headers = new Headers(request.headersIn as Record<string, string>)
return new RequestPolyfill(url.href, { headers: Object.fromEntries(headers) })
}
import RequestPolyfill from './request-polyfill'
describe('Request', () => {
it('constructor overloading - with url (string) as the 1st param', () => {
const request = new RequestPolyfill('http://example.com', {
headers: { foo: 'bar' },
})
expect(request.url).toBe('http://example.com')
expect(request.headers.get('foo')).toBe('bar')
})
it('constructor overloading - with Request as the 1st param', () => {
const request = new RequestPolyfill(
new RequestPolyfill('http://example.com', {}),
{
headers: { foo: 'bar' },
}
)
expect(request.url).toBe('http://example.com')
expect(request.headers.get('foo')).toBe('bar')
})
})
import { Headers } from 'headers-polyfill'
// Possible constructor overloading:
//
// 1. new Request(request.url, newRequestInit)
// 1. new Request(newUrl, { ...request, headers: newHeaders })
// 1. new Request(newUrl, request)
// 2. new Request(request, { headers: newHeaders })
class RequestPolyfill {
public readonly url: string
public readonly headers: Headers
constructor(url: string | RequestPolyfill, options?: RequestInit) {
if (url instanceof RequestPolyfill) {
this.url = url.url
this.headers = new Headers(options?.headers as Record<string, string>)
} else {
this.url = url
this.headers = new Headers(options?.headers as Record<string, string>)
}
}
toString(): string {
return 'URL = ' + this.url + '; headers = ' + this.headers
}
}
export default RequestPolyfill
import {
convertNjsResponseToCloudflare,
buildAndReturnNjsResponse,
} from './response-adaptor'
import ResponsePolyfill from './response-polyfill'
describe('response-adaptor', () => {
it('convertNjsResponseToCloudflare function', async () => {
const cloudflareResponse: ResponsePolyfill =
await convertNjsResponseToCloudflare({
headers: { foo: 'bar' },
status: 200,
statusText: 'OK',
arrayBuffer: () => Promise.resolve('test body'),
} as unknown as NgxResponse)
expect(cloudflareResponse.headers.get('foo')).toBe('bar')
expect(cloudflareResponse.status).toBe(200)
expect(cloudflareResponse.statusText).toBe('OK')
expect(cloudflareResponse.body).toBe('test body')
})
it('buildAndReturnNjsResponse function', async () => {
let mockStatus: undefined | number
let mockBody: undefined | NjsStringOrBuffer
const mockNgxRequest = {
headersOut: {},
return: (status: number, body: NjsStringOrBuffer) => {
mockStatus = status
mockBody = body
},
} as unknown as NginxHTTPRequest
buildAndReturnNjsResponse(
mockNgxRequest,
new ResponsePolyfill('test body', {
status: 200,
headers: { foo: 'bar' },
})
)
expect(mockNgxRequest.headersOut['foo']).toBe('bar')
expect(mockStatus).toBe(200)
expect(mockBody).toBe('test body')
})
})
import ResponsePolyfill from './response-polyfill'
function copyHeadersToNgxRequest(
ngxRequest: NginxHTTPRequest,
headers: Record<string, string>
) {
// In NJS, response headers are stored in ngxRequest.headersOut
// Copying headers here (ngxRequest.headersOut are immutable):
Object.entries(headers).forEach(([key, value]) => {
ngxRequest.headersOut[key] = value
})
}
/**
* @param ngxRequest - NJS request
* @param response - Cloudflare response
* @returns void
*/
function buildAndReturnNjsResponse(
ngxRequest: NginxHTTPRequest,
response: ResponsePolyfill
): void {
if (!response.status) {
throw new Error('Cloudflare response has missing status.')
}
copyHeadersToNgxRequest(ngxRequest, Object.fromEntries(response.headers))
if ([301, 302, 303, 307, 308].includes(response.status)) {
delete ngxRequest.headersOut["Location"]
ngxRequest.return(response.status, response.headers.get("Location") as NjsStringOrBuffer)
} else {
ngxRequest.return(response.status, response.body as NjsStringOrBuffer)
}
}
async function convertNjsResponseToCloudflare(
response: NgxResponse
): Promise<ResponsePolyfill> {
const buffer = await response.arrayBuffer()
return new ResponsePolyfill(buffer, {
...response,
headers: { ...response.headers } as unknown as Record<string, string>, // TODO find a better way
})
}
export { buildAndReturnNjsResponse, convertNjsResponseToCloudflare }
import ResponsePolyfill from './response-polyfill'
describe('Response', () => {
it('constructor should fill public fields', () => {
const response = new ResponsePolyfill('test body', {
status: 200,
statusText: 'OK',
headers: { foo: 'bar' },
})
expect(response.body).toBe('test body')
expect(response.status).toBe(200)
expect(response.statusText).toBe('OK')
expect(response.headers.get('foo')).toBe('bar')
})
})
import { Headers } from 'headers-polyfill'
class ResponsePolyfill implements Response {
public readonly headers: Headers
public readonly status: number
public readonly statusText: string
public readonly body: any
readonly ok: boolean
readonly trailer!: Promise<Headers>
readonly type!: ResponseType
readonly url!: string
readonly bodyUsed!: boolean
readonly redirected!: boolean
constructor(body?: BodyInit, init?: ResponseInit) {
this.headers = init?.headers ? new Headers(init.headers) : new Headers()
this.status = init?.status ?? 200
this.ok = this.status >= 200 && this.status <= 299
this.statusText = init?.statusText ?? ''
this.body = body
}
toString(): string {
return JSON.stringify(this)
}
arrayBuffer(): Promise<ArrayBuffer> {
throw new Error('Method not implemented.')
}
blob(): Promise<Blob> {
throw new Error('Method not implemented.')
}
clone(): Response {
throw new Error('Method not implemented.')
}
formData(): Promise<FormData> {
throw new Error('Method not implemented.')
}
json(): Promise<any> {
throw new Error('Method not implemented.')
}
text(): Promise<string> {
throw new Error('Method not implemented.')
}
}
export default ResponsePolyfill
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment