Skip to content

Commit 5a97c89

Browse files
authored
Merge pull request aws#6390 from jpinkney-aws/fetcher
refactor(core): Improve httpResourceFetcher API
2 parents 16c0228 + f727e0e commit 5a97c89

File tree

14 files changed

+49
-155
lines changed

14 files changed

+49
-155
lines changed

packages/core/src/lambda/utils.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import { CloudFormationClient } from '../shared/clients/cloudFormationClient'
1313
import { LambdaClient } from '../shared/clients/lambdaClient'
1414
import { getFamily, getNodeMajorVersion, RuntimeFamily } from './models/samLambdaRuntime'
1515
import { getLogger } from '../shared/logger'
16-
import { ResourceFetcher } from '../shared/resourcefetcher/resourcefetcher'
17-
import { CompositeResourceFetcher } from '../shared/resourcefetcher/compositeResourceFetcher'
1816
import { HttpResourceFetcher } from '../shared/resourcefetcher/httpResourceFetcher'
1917
import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetcher'
2018
import { sampleRequestManifestPath } from './constants'
@@ -99,7 +97,7 @@ interface SampleRequestManifest {
9997

10098
export async function getSampleLambdaPayloads(): Promise<SampleRequest[]> {
10199
const logger = getLogger()
102-
const sampleInput = await makeSampleRequestManifestResourceFetcher().get()
100+
const sampleInput = await getSampleRequestManifest()
103101

104102
if (!sampleInput) {
105103
throw new Error('Unable to retrieve Sample Request manifest')
@@ -120,9 +118,11 @@ export async function getSampleLambdaPayloads(): Promise<SampleRequest[]> {
120118
return inputs
121119
}
122120

123-
function makeSampleRequestManifestResourceFetcher(): ResourceFetcher {
124-
return new CompositeResourceFetcher(
125-
new HttpResourceFetcher(sampleRequestManifestPath, { showUrl: true }),
126-
new FileResourceFetcher(globals.manifestPaths.lambdaSampleRequests)
127-
)
121+
async function getSampleRequestManifest(): Promise<string | undefined> {
122+
const httpResp = await new HttpResourceFetcher(sampleRequestManifestPath, { showUrl: true }).get()
123+
if (!httpResp) {
124+
const fileResp = new FileResourceFetcher(globals.manifestPaths.lambdaSampleRequests)
125+
return fileResp.get()
126+
}
127+
return httpResp.text()
128128
}

packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ export class SamInvokeWebview extends VueWebview {
170170
return
171171
}
172172
const sampleUrl = `${sampleRequestPath}${pickerResponse.filename}`
173-
const sample = (await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get()) ?? ''
173+
const resp = await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get()
174+
const sample = (await resp?.text()) ?? ''
174175

175176
return sample
176177
} catch (err) {

packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@ export class RemoteInvokeWebview extends VueWebview {
219219
return
220220
}
221221
const sampleUrl = `${sampleRequestPath}${pickerResponse.filename}`
222-
const sample = (await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get()) ?? ''
222+
const resp = await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get()
223+
const sample = (await resp?.text()) ?? ''
223224

224225
return sample
225226
} catch (err) {

packages/core/src/shared/resourcefetcher/compositeResourceFetcher.ts

-34
This file was deleted.

packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts

+13-21
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import { getLogger, Logger } from '../logger'
88
import { ResourceFetcher } from './resourcefetcher'
99
import { Timeout, CancelEvent } from '../utilities/timeoutUtils'
1010
import request, { RequestError } from '../request'
11+
import { withRetries } from '../utilities/functionUtils'
1112

1213
type RequestHeaders = { eTag?: string; gZip?: boolean }
1314

14-
export class HttpResourceFetcher implements ResourceFetcher {
15+
export class HttpResourceFetcher implements ResourceFetcher<Response> {
1516
private readonly logger: Logger = getLogger()
1617

1718
/**
@@ -20,27 +21,27 @@ export class HttpResourceFetcher implements ResourceFetcher {
2021
* @param params Additional params for the fetcher
2122
* @param {boolean} params.showUrl Whether or not to the URL in log statements.
2223
* @param {string} params.friendlyName If URL is not shown, replaces the URL with this text.
23-
* @param {function} params.onSuccess Function to execute on successful request. No effect if piping to a location.
2424
* @param {Timeout} params.timeout Timeout token to abort/cancel the request. Similar to `AbortSignal`.
25+
* @param {number} params.retries The number of retries a get request should make if one fails
2526
*/
2627
public constructor(
2728
private readonly url: string,
2829
private readonly params: {
2930
showUrl: boolean
3031
friendlyName?: string
31-
onSuccess?(contents: string): void
3232
timeout?: Timeout
33+
retries?: number
3334
}
3435
) {}
3536

3637
/**
37-
* Returns the contents of the resource, or undefined if the resource could not be retrieved.
38-
*
39-
* @param pipeLocation Optionally pipe the download to a file system location
38+
* Returns the response of the resource, or undefined if the response failed could not be retrieved.
4039
*/
41-
public get(): Promise<string | undefined> {
40+
public get(): Promise<Response | undefined> {
4241
this.logger.verbose(`downloading: ${this.logText()}`)
43-
return this.downloadRequest()
42+
return withRetries(() => this.downloadRequest(), {
43+
maxRetries: this.params.retries ?? 1,
44+
})
4445
}
4546

4647
/**
@@ -69,26 +70,16 @@ export class HttpResourceFetcher implements ResourceFetcher {
6970
this.logger.verbose(`E-Tag, ${eTagResponse}, matched. No content downloaded from: ${this.url}`)
7071
} else {
7172
this.logger.verbose(`No E-Tag match. Downloaded content from: ${this.logText()}`)
72-
if (this.params.onSuccess) {
73-
this.params.onSuccess(contents)
74-
}
7573
}
7674

7775
return { content: contents, eTag: eTagResponse }
7876
}
7977

80-
private async downloadRequest(): Promise<string | undefined> {
78+
private async downloadRequest(): Promise<Response | undefined> {
8179
try {
82-
// HACK(?): receiving JSON as a string without `toString` makes it so we can't deserialize later
8380
const resp = await this.getResponseFromGetRequest(this.params.timeout)
84-
const contents = (await resp.text()).toString()
85-
if (this.params.onSuccess) {
86-
this.params.onSuccess(contents)
87-
}
88-
8981
this.logger.verbose(`downloaded: ${this.logText()}`)
90-
91-
return contents
82+
return resp
9283
} catch (err) {
9384
const error = err as RequestError
9485
this.logger.verbose(
@@ -150,7 +141,8 @@ export async function getPropertyFromJsonUrl(
150141
fetcher?: HttpResourceFetcher
151142
): Promise<any | undefined> {
152143
const resourceFetcher = fetcher ?? new HttpResourceFetcher(url, { showUrl: true })
153-
const result = await resourceFetcher.get()
144+
const resp = await resourceFetcher.get()
145+
const result = await resp?.text()
154146
if (result) {
155147
try {
156148
const json = JSON.parse(result)

packages/core/src/shared/resourcefetcher/resourcefetcher.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
*/
55

66
// TODO: this is just a "thunk". Replace it with something more generic.
7-
export interface ResourceFetcher {
7+
export interface ResourceFetcher<T = string> {
88
/**
99
* Returns the contents of the resource, or undefined if the resource could not be retrieved.
1010
* Implementations are expected to handle Errors.
1111
*/
12-
get(): Promise<string | undefined>
12+
get(): Promise<T | undefined>
1313
}

packages/core/src/shared/sam/debugger/csharpSamDebug.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ async function downloadInstallScript(debuggerPath: string): Promise<string> {
196196
installScriptPath = path.join(debuggerPath, 'installVsdbgScript.sh')
197197
}
198198

199-
const installScriptFetcher = new HttpResourceFetcher(installScriptUrl, { showUrl: true })
200-
const installScript = await installScriptFetcher.get()
199+
const installScriptFetcher = await new HttpResourceFetcher(installScriptUrl, { showUrl: true }).get()
200+
const installScript = await installScriptFetcher?.text()
201201
if (!installScript) {
202202
throw Error(`Failed to download ${installScriptUrl}`)
203203
}

packages/core/src/shared/schemas.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ export async function updateSchemaFromRemote(params: {
225225

226226
try {
227227
const httpFetcher = new HttpResourceFetcher(params.url, { showUrl: true })
228-
const content = await httpFetcher.get()
228+
const resp = await httpFetcher.get()
229+
const content = await resp?.text()
229230

230231
if (!content) {
231232
throw new Error(`failed to resolve schema: ${params.destination}`)

packages/core/src/stepFunctions/utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ async function httpsGetRequestWrapper(url: string): Promise<string> {
179179
const logger = getLogger()
180180
logger.verbose('Step Functions is getting content...')
181181

182-
const fetcher = new HttpResourceFetcher(url, { showUrl: true })
183-
const val = await fetcher.get()
182+
const fetcher = await new HttpResourceFetcher(url, { showUrl: true }).get()
183+
const val = await fetcher?.text()
184184

185185
if (!val) {
186186
const message = 'Step Functions was unable to get content.'

packages/core/src/test/lambda/vue/remoteInvoke/invokeLambda.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { FakeExtensionContext } from '../../../fakeExtensionContext'
2121
import * as samCliRemoteTestEvent from '../../../../shared/sam/cli/samCliRemoteTestEvent'
2222
import { TestEventsOperation, SamCliRemoteTestEventsParameters } from '../../../../shared/sam/cli/samCliRemoteTestEvent'
2323
import { assertLogsContain } from '../../../globalSetup.test'
24+
import { createResponse } from '../../../testUtil'
2425

2526
describe('RemoteInvokeWebview', () => {
2627
let outputChannel: vscode.OutputChannel
@@ -394,7 +395,7 @@ describe('RemoteInvokeWebview', () => {
394395
createQuickPickStub.returns({})
395396
promptUserStub.resolves([{ label: 'testEvent', filename: 'testEvent.json' }])
396397
verifySinglePickerOutputStub.returns({ label: 'testEvent', filename: 'testEvent.json' })
397-
httpFetcherStub.resolves(mockSampleContent)
398+
httpFetcherStub.resolves(createResponse(mockSampleContent))
398399

399400
const result = await remoteInvokeWebview.getSamplePayload()
400401

packages/core/src/test/lambda/vue/samInvokeBackend.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { SamDebugConfigProvider } from '../../../shared/sam/debugger/awsSamDebug
2828
import sinon from 'sinon'
2929
import * as nls from 'vscode-nls'
3030
import { assertLogsContain } from '../../../test/globalSetup.test'
31+
import { createResponse } from '../../testUtil'
3132

3233
const localize = nls.loadMessageBundle()
3334

@@ -166,7 +167,7 @@ describe('SamInvokeWebview', () => {
166167
createQuickPickStub.returns({})
167168
promptUserStub.resolves([{ label: 'testEvent', filename: 'testEvent.json' }])
168169
verifySinglePickerOutputStub.returns({ label: 'testEvent', filename: 'testEvent.json' })
169-
httpFetcherStub.resolves(mockSampleContent)
170+
httpFetcherStub.resolves(createResponse(mockSampleContent))
170171

171172
const result = await samInvokeWebview.getSamplePayload()
172173

packages/core/src/test/shared/resourceFetcher/compositeResourceFetcher.test.ts

-77
This file was deleted.

packages/core/src/test/shared/resourceFetcher/httpResourceFetcher.test.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import assert from 'assert'
77
import { HttpResourceFetcher, getPropertyFromJsonUrl } from '../../../shared/resourcefetcher/httpResourceFetcher'
88
import { stub } from '../../utilities/stubber'
9+
import { createResponse } from '../../testUtil'
910

1011
describe('getPropertyFromJsonUrl', function () {
1112
const dummyUrl = 'url'
@@ -19,18 +20,18 @@ describe('getPropertyFromJsonUrl', function () {
1920

2021
it('undefined if resource is not JSON', async function () {
2122
const mockFetcher = stub(HttpResourceFetcher)
22-
mockFetcher.get.resolves('foo' as any) // horrible hack: this works without the declaration but the language server latches onto this using a FetcherResult return type
23+
mockFetcher.get.resolves(createResponse('foo'))
2324
assert.strictEqual(await getPropertyFromJsonUrl(dummyUrl, dummyProperty, mockFetcher), undefined)
2425
})
2526

2627
it('undefined if property is not present', async function () {
2728
const mockFetcher = stub(HttpResourceFetcher)
28-
mockFetcher.get.resolves('{"foo": "bar"}' as any)
29+
mockFetcher.get.resolves(createResponse('{"foo": "bar"}'))
2930
assert.strictEqual(await getPropertyFromJsonUrl(dummyUrl, dummyProperty, mockFetcher), undefined)
3031
})
3132
it('returns value if property is present', async function () {
3233
const mockFetcher = stub(HttpResourceFetcher)
33-
mockFetcher.get.resolves('{"property": "111"}' as any)
34+
mockFetcher.get.resolves(createResponse('{"property": "111"}'))
3435
mockFetcher.getNewETagContent.resolves({ content: 'foo', eTag: '' })
3536
const value = await getPropertyFromJsonUrl(dummyUrl, dummyProperty, mockFetcher)
3637
assert.strictEqual(value, '111')

packages/core/src/test/testUtil.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { DeclaredCommand } from '../shared/vscode/commands2'
1818
import { mkdirSync, existsSync } from 'fs' // eslint-disable-line no-restricted-imports
1919
import { randomBytes } from 'crypto'
2020
import request from '../shared/request'
21-
import { stub } from 'sinon'
21+
import { createStubInstance, stub } from 'sinon'
2222

2323
const testTempDirs: string[] = []
2424

@@ -634,3 +634,10 @@ export function getFetchStubWithResponse(response: Partial<Response>) {
634634
export function copyEnv(): NodeJS.ProcessEnv {
635635
return { ...process.env }
636636
}
637+
638+
// Returns a stubbed response object
639+
export function createResponse(text: string): Response {
640+
const responseStub = createStubInstance(Response)
641+
responseStub.text.resolves(text)
642+
return responseStub
643+
}

0 commit comments

Comments
 (0)