Skip to content

Commit

Permalink
Merge pull request #1460 from nextstrain/feat/fetch-auspice-sidecar-json
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-aksamentov authored May 30, 2024
2 parents 9a172d1 + 6e964be commit f17e687
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 12 deletions.
3 changes: 3 additions & 0 deletions packages/nextclade-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
},
"dependencies": {
"@floating-ui/react": "0.26.1",
"@hapi/accept": "6.0.3",
"@hapi/content": "6.0.0",
"animate.css": "4.1.1",
"auspice": "2.53.0",
"autoprefixer": "10.4.5",
Expand Down Expand Up @@ -200,6 +202,7 @@
"@types/friendly-errors-webpack-plugin": "0.1.4",
"@types/fs-extra": "9.0.13",
"@types/glob": "7.2.0",
"@types/hapi__content": "6.0.3",
"@types/history": "4.7.11",
"@types/intercept-stdout": "0.1.0",
"@types/jest": "27.4.1",
Expand Down
53 changes: 44 additions & 9 deletions packages/nextclade-web/src/io/axiosFetch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
import { isNil } from 'lodash'
import { isNil, isString } from 'lodash'
import { mediaTypes as parseAcceptHeader } from '@hapi/accept'
import { ContentType, type as parseContentTypeHeader } from '@hapi/content'
import { ErrorFatal } from 'src/helpers/ErrorFatal'
import { sanitizeError } from 'src/helpers/sanitizeError'

export interface RequestConfig extends AxiosRequestConfig {
// Check that MIME type of response's Content-Type header is compatible with at least one of MIME types in the request's Accept header
strictAccept?: boolean
}

export class HttpRequestError extends Error {
public readonly url?: string
public readonly status?: number | string
Expand Down Expand Up @@ -36,10 +43,27 @@ export function validateUrl(url?: string): string {
return url
}

export async function axiosFetch<TData = unknown>(
url_: string | undefined,
options?: AxiosRequestConfig,
): Promise<TData> {
export interface CheckMimeTypesResult {
isCompatible: boolean
acceptTypes?: string[]
contentType?: ContentType
acceptHeader?: string | number | boolean
contentTypeHeader?: string
}

export function checkMimeTypes(req?: RequestConfig, res?: AxiosResponse): CheckMimeTypesResult {
const acceptHeader = req?.headers?.Accept
const contentTypeHeader = res?.headers['content-type']
if (isString(acceptHeader) && isString(contentTypeHeader)) {
const acceptTypes = parseAcceptHeader(acceptHeader)
const contentType = parseContentTypeHeader(contentTypeHeader)
const isCompatible = acceptTypes.includes(contentType.mime)
return { isCompatible, acceptTypes, contentType, acceptHeader, contentTypeHeader }
}
return { isCompatible: false, acceptHeader, contentTypeHeader }
}

export async function axiosFetch<TData = unknown>(url_: string | undefined, options?: RequestConfig): Promise<TData> {
const url = validateUrl(url_)

let res
Expand All @@ -53,6 +77,17 @@ export async function axiosFetch<TData = unknown>(
throw new Error(`Unable to fetch: request to URL "${url}" resulted in no data`)
}

if (options?.strictAccept) {
const mime = checkMimeTypes(options, res)
if (!mime.isCompatible) {
const accept = mime.acceptHeader ?? ''
const contentType = mime.contentTypeHeader ?? ''
throw new Error(
`Unable to fetch: request to URL "${url}" resulted in incompatible MIME type: Content-Type was "${contentType}", while Accept was "${accept}"`,
)
}
}

return res.data as TData
}

Expand All @@ -65,7 +100,7 @@ export async function axiosFetchMaybe(url?: string): Promise<string | undefined>

export async function axiosFetchOrUndefined<TData = unknown>(
url: string | undefined,
options?: AxiosRequestConfig,
options?: RequestConfig,
): Promise<TData | undefined> {
try {
return await axiosFetch<TData>(url, options)
Expand All @@ -77,7 +112,7 @@ export async function axiosFetchOrUndefined<TData = unknown>(
/**
* This version skips any transforms (such as JSON parsing) and returns plain string
*/
export async function axiosFetchRaw(url: string | undefined, options?: AxiosRequestConfig): Promise<string> {
export async function axiosFetchRaw(url: string | undefined, options?: RequestConfig): Promise<string> {
return axiosFetch(url, { ...options, transformResponse: [] })
}

Expand All @@ -88,7 +123,7 @@ export async function axiosFetchRawMaybe(url?: string): Promise<string | undefin
return axiosFetchRaw(url)
}

export async function axiosHead(url: string | undefined, options?: AxiosRequestConfig): Promise<AxiosResponse> {
export async function axiosHead(url: string | undefined, options?: RequestConfig): Promise<AxiosResponse> {
if (isNil(url)) {
throw new ErrorFatal(`Attempted to fetch from an invalid URL: '${url}'`)
}
Expand All @@ -102,7 +137,7 @@ export async function axiosHead(url: string | undefined, options?: AxiosRequestC

export async function axiosHeadOrUndefined(
url: string | undefined,
options?: AxiosRequestConfig,
options?: RequestConfig,
): Promise<AxiosResponse | undefined> {
try {
return await axiosHead(url, options)
Expand Down
18 changes: 16 additions & 2 deletions packages/nextclade-web/src/io/fetchSingleDatasetAuspice.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { isEmpty } from 'lodash'
import { attrStrMaybe, AuspiceTree, Dataset } from 'src/types'
import { removeTrailingSlash } from 'src/io/url'
import { axiosFetch } from 'src/io/axiosFetch'
import { axiosFetch, axiosFetchOrUndefined } from 'src/io/axiosFetch'

export async function fetchSingleDatasetAuspice(datasetJsonUrl_: string) {
const datasetJsonUrl = removeTrailingSlash(datasetJsonUrl_)

const auspiceJson = await axiosFetch<AuspiceTree>(datasetJsonUrl, {
headers: { Accept: 'application/json, text/plain, */*' },
headers: {
Accept: 'application/vnd.nextstrain.dataset.main+json;q=1, application/json;q=0.9, text/plain;q=0.8, */*;q=0.1',
},
})

if (isEmpty(auspiceJson.root_sequence)) {
const sidecar = await axiosFetchOrUndefined<Record<string, string>>(datasetJsonUrl, {
headers: { Accept: 'application/vnd.nextstrain.dataset.root-sequence+json' },
strictAccept: true,
})
if (!isEmpty(sidecar)) {
auspiceJson.root_sequence = sidecar
}
}

const pathogen = auspiceJson.meta.extensions?.nextclade?.pathogen

const name =
Expand Down
4 changes: 3 additions & 1 deletion packages/nextclade-web/src/styles/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

@import './components/Results';

html, body, #__next {
html,
body,
#__next {
overflow: hidden;
height: 100%;
width: 100%;
Expand Down
34 changes: 34 additions & 0 deletions packages/nextclade-web/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2365,6 +2365,33 @@
protobufjs "^7.0.0"
yargs "^16.2.0"

"@hapi/[email protected]":
version "6.0.3"
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-6.0.3.tgz#eef0800a4f89cd969da8e5d0311dc877c37279ab"
integrity sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==
dependencies:
"@hapi/boom" "^10.0.1"
"@hapi/hoek" "^11.0.2"

"@hapi/boom@^10.0.0", "@hapi/boom@^10.0.1":
version "10.0.1"
resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.1.tgz#ebb14688275ae150aa6af788dbe482e6a6062685"
integrity sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==
dependencies:
"@hapi/hoek" "^11.0.2"

"@hapi/[email protected]":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@hapi/content/-/content-6.0.0.tgz#2427af3bac8a2f743512fce2a70cbdc365af29df"
integrity sha512-CEhs7j+H0iQffKfe5Htdak5LBOz/Qc8TRh51cF+BFv0qnuph3Em4pjGVzJMkI2gfTDdlJKWJISGWS1rK34POGA==
dependencies:
"@hapi/boom" "^10.0.0"

"@hapi/hoek@^11.0.2":
version "11.0.4"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.4.tgz#42a7f244fd3dd777792bfb74b8c6340ae9182f37"
integrity sha512-PnsP5d4q7289pS2T2EgGz147BFJ2Jpb4yrEdkpz2IhgEUzos1S7HTl7ezWh1yfYzYlj89KzLdCRkqsP6SIryeQ==

"@hot-loader/react-dom@^16.13.0":
version "16.14.0"
resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.14.0.tgz#3cfc64e40bb78fa623e59b582b8f09dcdaad648a"
Expand Down Expand Up @@ -3434,6 +3461,13 @@
dependencies:
"@types/node" "*"

"@types/[email protected]":
version "6.0.3"
resolved "https://registry.yarnpkg.com/@types/hapi__content/-/hapi__content-6.0.3.tgz#4e3820d8d07ae90de955263622e9c56ee177bd01"
integrity sha512-J0TOzZIy99b9G2ujGXpC7369wohXLCLwNfxEFN+X1w0oYOEhQ5+ViKweCZ919fGVp9U40QAjUc/LD1h1CVFCpQ==
dependencies:
"@types/node" "*"

"@types/hast@^2.0.0":
version "2.3.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"
Expand Down

0 comments on commit f17e687

Please sign in to comment.