Skip to content

Commit

Permalink
fix: non-JSON response body display for upload errors (#29801)
Browse files Browse the repository at this point in the history
* WIP - system test snapshots

* bolster error output for response body potential on upload network errors

* edit error output for readability

* changelog

* Update packages/server/lib/cloud/artifacts/upload_artifacts.ts

Co-authored-by: Matt Schile <mschile@cypress.io>

* fix partial rename

* fix system test for single upload error vs aggregate

---------

Co-authored-by: Matt Schile <mschile@cypress.io>
Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
  • Loading branch information
3 people committed Jul 12, 2024
1 parent 807b03d commit 3c28109
Show file tree
Hide file tree
Showing 11 changed files with 196 additions and 107 deletions.
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ _Released 7/16/2024 (PENDING)_

**Bugfixes:**

- CLI output properly displays non-JSON response bodies when a Test Replay upload attempt returns a non-JSON response body for a non-200 status code. Addressed in [#29801](https://github.com/cypress-io/cypress/pull/29801).
- Fixed an issue where the ReadStream used to upload a Test Replay recording could erroneously be re-used when retrying in cases of retryable upload failures. Fixes [#29227](https://github.com/cypress-io/cypress/issues/29227).
- Fixed an issue where command snapshots were not being captured within the `cy.origin()` command within Test Replay. Addressed in [#29828](https://github.com/cypress-io/cypress/pull/29828).

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 15 additions & 7 deletions packages/errors/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,15 +574,17 @@ export const AllCypressErrors = {
This error will not affect or change the exit code.
`
},
CLOUD_PROTOCOL_UPLOAD_HTTP_FAILURE: (error: Error & { url: string, status: number, statusText: string }) => {
CLOUD_PROTOCOL_UPLOAD_HTTP_FAILURE: (error: Error & { url: string, status: number, statusText: string, responseBody: string }) => {
return errTemplate`\
Warning: We encountered an HTTP error while uploading the Test Replay recording for this spec.
These results will not display Test Replay recordings.
This error will not affect or change the exit code.
${fmt.url(error.url)} responded with HTTP ${fmt.stringify(error.status)}: ${fmt.highlightSecondary(error.statusText)}`
${fmt.url(error.url)} responded with HTTP ${fmt.stringify(error.status)}: ${fmt.highlightSecondary(error.statusText)}
${fmt.highlightTertiary(error.responseBody)}`
},
CLOUD_PROTOCOL_UPLOAD_NEWORK_FAILURE: (error: Error & { url: string }) => {
return errTemplate`\
Expand All @@ -597,29 +599,35 @@ export const AllCypressErrors = {
${fmt.highlightSecondary(error)}`
},
CLOUD_PROTOCOL_UPLOAD_AGGREGATE_ERROR: (error: {
errors: (Error & { kind?: 'NetworkError' | 'HttpError', url: string })[]
errors: (Error & { kind?: 'NetworkError', url: string } | Error & { kind: 'HttpError', url: string, status?: string, statusText?: string, responseBody?: string })[]
}) => {
if (error.errors.length === 1) {
if (error.errors[0]?.kind === 'NetworkError') {
return AllCypressErrors.CLOUD_PROTOCOL_UPLOAD_NEWORK_FAILURE(error.errors[0])
const firstError = error.errors[0]

if (firstError?.kind === 'NetworkError') {
return AllCypressErrors.CLOUD_PROTOCOL_UPLOAD_NEWORK_FAILURE(firstError as Error & { url: string })
}

return AllCypressErrors.CLOUD_PROTOCOL_UPLOAD_HTTP_FAILURE(error.errors[0] as Error & { url: string, status: number, statusText: string})
return AllCypressErrors.CLOUD_PROTOCOL_UPLOAD_HTTP_FAILURE(error.errors[0] as Error & { url: string, status: number, statusText: string, responseBody: string})
}

let networkErr = error.errors.find((err) => {
return err.kind === 'NetworkError'
})
const recommendation = networkErr ? errPartial`Some or all of the errors encountered are system-level network errors. Please verify your network configuration for connecting to ${fmt.highlightSecondary(networkErr.url)}` : null

const fmtUploadError = ({ message, responseBody }: { message: string, responseBody?: string }) => {
return `${message}${responseBody ? `:\n${responseBody}\n` : ''}`
}

return errTemplate`\
Warning: We encountered multiple errors while uploading the Test Replay recording for this spec.
We attempted to upload the Test Replay recording ${fmt.stringify(error.errors.length)} times.
${recommendation}
${fmt.listItems(error.errors.map((error) => error.message))}`
${fmt.listItems(error.errors.map(fmtUploadError), { prefix: '' })}`
},
CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE: (apiErr: Error) => {
return errTemplate`\
Expand Down
3 changes: 2 additions & 1 deletion packages/errors/test/unit/visualSnapshotErrors_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,11 +674,12 @@ describe('visual error templates', () => {
},
CLOUD_PROTOCOL_UPLOAD_HTTP_FAILURE: () => {
// @ts-expect-error
const err: Error & { status: number, statusText: string, url: string } = makeErr()
const err: Error & { status: number, statusText: string, url: string, message: string, responseBody: string } = makeErr()

err.status = 500
err.statusText = 'Internal Server Error'
err.url = 'https://some/url'
err.responseBody = '{ status: 500, reason: \'unknown\'}'

return {
default: [err],
Expand Down
18 changes: 16 additions & 2 deletions packages/server/lib/cloud/artifacts/upload_artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ import { NetworkError } from '../network/network_error'

const debug = Debug('cypress:server:cloud:artifacts')

const removeWhitespaceAndTrim = (str: string) => {
return str.split(/\n/)
.map((line) => {
return line.trim()
})
.join('')
}

const toUploadReportPayload = (acc: {
screenshots: ArtifactMetadata[]
video?: ArtifactMetadata
Expand All @@ -27,8 +35,14 @@ const toUploadReportPayload = (acc: {
let { error, errorStack, allErrors } = reportWithoutOriginalError

if (allErrors) {
error = `Failed to upload Test Replay after ${allErrors.length} attempts. Errors: ${allErrors.map((error) => error.message).join(', ')}`
errorStack = allErrors.map((error) => error.stack).join(', ')
const messages = allErrors.map((error) => {
return (HttpError.isHttpError(error) && error.responseBody) ?
`${error.message}: ${removeWhitespaceAndTrim(error.responseBody)}` :
error.message
})

error = `Failed to upload Test Replay after ${allErrors.length} attempts. Errors: ${messages.join(', ')}`
errorStack = allErrors.map((error) => error.stack).join('')
} else if (error) {
error = `Failed to upload Test Replay: ${error}`
}
Expand Down
9 changes: 5 additions & 4 deletions packages/server/lib/cloud/network/http_error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class HttpError extends Error {
public readonly url: string,
public readonly status: number,
public readonly statusText: string,
public readonly responseBody: string,
public readonly originalResponse: Response,
) {
super(message)
Expand All @@ -20,16 +21,16 @@ export class HttpError extends Error {

public static async fromResponse (response: Response): Promise<HttpError> {
const status = response.status
const statusText = await (response.json().catch(() => {
return response.statusText
}))
const statusText = response.statusText
const responseBody = await response.text()
const scrubbedUrl = scrubUrl(response.url)

return new HttpError(
`${status} ${statusText} (${scrubUrl(response.url)})`,
`${scrubUrl(response.url)} responded with ${status} ${statusText}`,
scrubbedUrl,
status,
statusText,
responseBody,
response,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ describe('isRetryableError', () => {

it('returns true with retryable http errors', () => {
[408, 429, 502, 503, 504].forEach((status) => {
const err = new HttpError('some error', url, status, 'status text', sinon.createStubInstance(Response))
const err = new HttpError('some error', url, status, 'status text', 'response_body', sinon.createStubInstance(Response))

expect(isRetryableError(err)).to.be.true
})
})

it('returns false with non-retryable http errors', () => {
[400, 401, 402, 403, 404, 405, 406, 407, 409, 410, 411, 412, 413, 414, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 431, 451, 500, 501, 505, 507, 508, 510, 511].forEach((status) => {
const err = new HttpError('some error', url, status, 'status text', sinon.createStubInstance(Response))
const err = new HttpError('some error', url, status, 'status text', 'response_body', sinon.createStubInstance(Response))

expect(isRetryableError(err)).to.be.false
})
Expand Down
Loading

5 comments on commit 3c28109

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 3c28109 Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.13.1/linux-arm64/develop-3c28109185d9a04a4098a5b21be374213fc8103c/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 3c28109 Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.13.1/linux-x64/develop-3c28109185d9a04a4098a5b21be374213fc8103c/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 3c28109 Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.13.1/win32-x64/develop-3c28109185d9a04a4098a5b21be374213fc8103c/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 3c28109 Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.13.1/darwin-x64/develop-3c28109185d9a04a4098a5b21be374213fc8103c/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 3c28109 Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.13.1/darwin-arm64/develop-3c28109185d9a04a4098a5b21be374213fc8103c/cypress.tgz

Please sign in to comment.