Report detailed blocked reason in headers view
This adds detailed explanations to the headers view component for
requests blocked by COEP (originally added in
https://crrev.com/c/2083095).
Screenshot: https://i.imgur.com/i71ibMl.png
Bug: 1297533
Change-Id: Ia5fc6f137492f81baa6d70ce927696f96c2b4d61
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3740584
Commit-Queue: Wolfgang Beyer <[email protected]>
Reviewed-by: Danil Somsikov <[email protected]>
diff --git a/front_end/panels/network/components/BUILD.gn b/front_end/panels/network/components/BUILD.gn
index 6158983..96f8e4a 100644
--- a/front_end/panels/network/components/BUILD.gn
+++ b/front_end/panels/network/components/BUILD.gn
@@ -23,9 +23,13 @@
]
deps = [
+ "../../../core/common:bundle",
+ "../../../core/host:bundle",
"../../../core/i18n:bundle",
+ "../../../core/platform:bundle",
"../../../core/sdk:bundle",
"../../../generated:protocol",
+ "../../../models/issues_manager:bundle",
"../../../panels/utils:bundle",
"../../../ui/components/buttons:bundle",
"../../../ui/components/data_grid:bundle",
diff --git a/front_end/panels/network/components/RequestHeadersView.css b/front_end/panels/network/components/RequestHeadersView.css
index d5b2ed7..36693ea 100644
--- a/front_end/panels/network/components/RequestHeadersView.css
+++ b/front_end/panels/network/components/RequestHeadersView.css
@@ -109,3 +109,58 @@
white-space: pre-wrap;
word-break: break-all;
}
+
+.header-badge-text {
+ font-variant: small-caps;
+ font-weight: 500;
+ white-space: pre-wrap;
+ word-break: break-all;
+}
+
+.header-badge {
+ display: inline;
+ margin-right: 0.75em;
+ background-color: var(--color-accent-red);
+ color: var(--color-background);
+ border-radius: 100vh;
+ padding-left: 6px;
+ padding-right: 6px;
+}
+
+.call-to-action {
+ background-color: var(--color-background-elevation-1);
+ padding: 8px;
+ border-radius: 2px;
+}
+
+.call-to-action-body {
+ padding: 6px 0;
+ margin-left: 9.5px;
+ border-left: 2px solid var(--issue-color-yellow);
+ padding-left: 18px;
+ line-height: 20px;
+}
+
+.call-to-action .explanation {
+ font-weight: bold;
+}
+
+.call-to-action code {
+ font-size: 90%;
+}
+
+.call-to-action .example .comment::before {
+ content: " — ";
+}
+
+.link,
+.devtools-link {
+ color: var(--color-link);
+ text-decoration: underline;
+ cursor: pointer;
+ padding: 2px 0; /* adjust focus ring size */
+}
+
+.inline-icon {
+ vertical-align: middle;
+}
diff --git a/front_end/panels/network/components/RequestHeadersView.ts b/front_end/panels/network/components/RequestHeadersView.ts
index 3332ca2..e422f8c 100644
--- a/front_end/panels/network/components/RequestHeadersView.ts
+++ b/front_end/panels/network/components/RequestHeadersView.ts
@@ -3,11 +3,16 @@
// found in the LICENSE file.
import * as Common from '../../../core/common/common.js';
+import * as Host from '../../../core/host/host.js';
import * as i18n from '../../../core/i18n/i18n.js';
+import * as Platform from '../../../core/platform/platform.js';
import {assertNotNullOrUndefined} from '../../../core/platform/platform.js';
import * as SDK from '../../../core/sdk/sdk.js';
+import * as Protocol from '../../../generated/protocol.js';
+import * as IssuesManager from '../../../models/issues_manager/issues_manager.js';
import * as Buttons from '../../../ui/components/buttons/buttons.js';
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
+import * as IconButton from '../../../ui/components/icon_button/icon_button.js';
import * as Input from '../../../ui/components/input/input.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as LitHtml from '../../../ui/lit-html/lit-html.js';
@@ -19,6 +24,11 @@
const UIStrings = {
/**
+ *@description Text in Headers View of the Network panel
+ */
+ chooseThisOptionIfTheResourceAnd:
+ 'Choose this option if the resource and the document are served from the same site.',
+ /**
*@description Text in Request Headers View of the Network panel
*/
fromDiskCache: '(from disk cache)',
@@ -47,10 +57,23 @@
*/
general: 'General',
/**
+ *@description Text that is usually a hyperlink to more documentation
+ */
+ learnMore: 'Learn more',
+ /**
+ *@description Text for a link to the issues panel
+ */
+ learnMoreInTheIssuesTab: 'Learn more in the issues tab',
+ /**
*@description Label for a checkbox to switch between raw and parsed headers
*/
raw: 'Raw',
/**
+ *@description Text in Headers View of the Network panel
+ */
+ onlyChooseThisOptionIfAn:
+ 'Only choose this option if an arbitrary website including this resource does not impose a security risk.',
+ /**
*@description Text in Request Headers View of the Network panel
*/
referrerPolicy: 'Referrer Policy',
@@ -82,9 +105,35 @@
*@description HTTP response code
*/
statusCode: 'Status Code',
+ /**
+ *@description Text in Headers View of the Network panel
+ */
+ thisDocumentWasBlockedFrom:
+ 'This document was blocked from loading in an `iframe` with a `sandbox` attribute because this document specified a cross-origin opener policy.',
+ /**
+ *@description Text in Headers View of the Network panel
+ */
+ toEmbedThisFrameInYourDocument:
+ 'To embed this frame in your document, the response needs to enable the cross-origin embedder policy by specifying the following response header:',
+ /**
+ *@description Text in Headers View of the Network panel
+ */
+ toUseThisResourceFromADifferent:
+ 'To use this resource from a different origin, the server needs to specify a cross-origin resource policy in the response headers:',
+ /**
+ *@description Text in Headers View of the Network panel
+ */
+ toUseThisResourceFromADifferentOrigin:
+ 'To use this resource from a different origin, the server may relax the cross-origin resource policy response header:',
+ /**
+ *@description Text in Headers View of the Network panel
+ */
+ toUseThisResourceFromADifferentSite:
+ 'To use this resource from a different site, the server may relax the cross-origin resource policy response header:',
};
const str_ = i18n.i18n.registerUIStrings('panels/network/components/RequestHeadersView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
+const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
export class RequestHeadersView extends UI.Widget.VBox {
readonly #headersView = new RequestHeadersComponent();
@@ -152,6 +201,39 @@
#renderResponseHeaders(): LitHtml.TemplateResult {
assertNotNullOrUndefined(this.#request);
+ const headersWithIssues = [];
+ if (this.#request.wasBlocked()) {
+ const headerWithIssues =
+ BlockedReasonDetails.get((this.#request.blockedReason() as Protocol.Network.BlockedReason));
+ if (headerWithIssues) {
+ headersWithIssues.push(headerWithIssues);
+ }
+ }
+
+ function mergeHeadersWithIssues(
+ headers: SDK.NetworkRequest.NameValue[], headersWithIssues: HeaderDescriptor[]): HeaderDescriptor[] {
+ let i = 0, j = 0;
+ const result: HeaderDescriptor[] = [];
+ while (i < headers.length && j < headersWithIssues.length) {
+ if (headers[i].name < headersWithIssues[j].name) {
+ result.push({...headers[i++], headerNotSet: false});
+ } else if (headers[i].name > headersWithIssues[j].name) {
+ result.push({...headersWithIssues[j++], headerNotSet: true});
+ } else {
+ result.push({...headersWithIssues[j++], ...headers[i++], headerNotSet: false});
+ }
+ }
+ while (i < headers.length) {
+ result.push({...headers[i++], headerNotSet: false});
+ }
+ while (j < headersWithIssues.length) {
+ result.push({...headersWithIssues[j++], headerNotSet: true});
+ }
+ return result;
+ }
+
+ const mergedHeaders = mergeHeadersWithIssues(this.#request.sortedResponseHeaders.slice(), headersWithIssues);
+
const toggleShowRaw = (): void => {
this.#showResponseHeadersText = !this.#showResponseHeadersText;
this.#render();
@@ -172,12 +254,7 @@
>
${this.#showResponseHeadersText ?
this.#renderRawHeaders(this.#request.responseHeadersText, true) : html`
- ${this.#request.sortedResponseHeaders.map(header => html`
- <div class="row">
- <div class="header-name">${header.name}:</div>
- <div class="header-value">${header.value}</div>
- </div>
- `)}
+ ${mergedHeaders.map(header => this.#renderHeader(header))}
`}
</${Category.litTagName}>
`;
@@ -186,13 +263,17 @@
#renderRequestHeaders(): LitHtml.TemplateResult {
assertNotNullOrUndefined(this.#request);
+ const headers = this.#request.requestHeaders().slice();
+ headers.sort(function(a, b) {
+ return Platform.StringUtilities.compare(a.name.toLowerCase(), b.name.toLowerCase());
+ });
+ const requestHeadersText = this.#request.requestHeadersText();
+
const toggleShowRaw = (): void => {
this.#showRequestHeadersText = !this.#showRequestHeadersText;
this.#render();
};
- const requestHeadersText = this.#request.requestHeadersText();
-
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
return html`
@@ -208,17 +289,85 @@
>
${(this.#showRequestHeadersText && requestHeadersText) ?
this.#renderRawHeaders(requestHeadersText, false) : html`
- ${this.#request.requestHeaders().map(header => html`
- <div class="row">
- <div class="header-name">${header.name}:</div>
- <div class="header-value">${header.value}</div>
- </div>
- `)}
+ ${headers.map(header => this.#renderHeader({...header, headerNotSet: false}))}
`}
</${Category.litTagName}>
`;
}
+ #renderHeader(header: HeaderDescriptor): LitHtml.TemplateResult {
+ return html`
+ <div class="row">
+ <div class="header-name">${header.headerNotSet ? html`<div class="header-badge header-badge-text">not-set</div>` : ''}${header.name}:</div>
+ <div class="header-value ${header.headerValueIncorrect ? 'header-warning' : ''}">${header.value?.toString()||''}</div>
+ </div>
+ ${this.#maybeRenderHeaderDetails(header.details)}
+ `;
+ }
+
+ #maybeRenderHeaderDetails(headerDetails?: HeaderDetailsDescriptor): LitHtml.LitTemplate {
+ if (!headerDetails) {
+ return LitHtml.nothing;
+ }
+ return html`
+ <div class="header-details">
+ <div class="call-to-action">
+ <div class="call-to-action-body">
+ <div class="explanation">${headerDetails.explanation()}</div>
+ ${headerDetails.examples.map(example => html`
+ <div class="example">
+ <code>${example.codeSnippet}</code>
+ ${example.comment ? html`
+ <span class="comment">${example.comment()}</span>
+ ` : ''}
+ </div>
+ `)}
+ ${this.#maybeRenderHeaderDetailsLink(headerDetails)}
+ </div>
+ </div>
+ </div>
+ `;
+ }
+
+ #maybeRenderHeaderDetailsLink(headerDetails?: HeaderDetailsDescriptor): LitHtml.LitTemplate {
+ if (this.#request && IssuesManager.RelatedIssue.hasIssueOfCategory(this.#request, IssuesManager.Issue.IssueCategory.CrossOriginEmbedderPolicy)) {
+ const followLink = (): void => {
+ Host.userMetrics.issuesPanelOpenedFrom(Host.UserMetrics.IssueOpener.LearnMoreLinkCOEP);
+ if (this.#request) {
+ void IssuesManager.RelatedIssue.reveal(
+ this.#request, IssuesManager.Issue.IssueCategory.CrossOriginEmbedderPolicy);
+ }
+ };
+ return html`
+ <div class="devtools-link" @click=${followLink}>
+ <${IconButton.Icon.Icon.litTagName} class="inline-icon" .data=${{
+ iconName: 'issue-exclamation-icon',
+ color: 'var(--issue-color-yellow)',
+ width: '16px',
+ height: '16px',
+ } as IconButton.Icon.IconData}>
+ </${IconButton.Icon.Icon.litTagName}>
+ ${i18nString(UIStrings.learnMoreInTheIssuesTab)}
+ </div>
+ `;
+ }
+ if (headerDetails?.link) {
+ return html`
+ <x-link href=${headerDetails.link.url} class="link">
+ <${IconButton.Icon.Icon.litTagName} class="inline-icon" .data=${{
+ iconName: 'link_icon',
+ color: 'var(--color-link)',
+ width: '16px',
+ height: '16px',
+ } as IconButton.Icon.IconData}>
+ </${IconButton.Icon.Icon.litTagName}
+ >${i18nString(UIStrings.learnMore)}
+ </x-link>
+ `;
+ }
+ return LitHtml.nothing;
+ }
+
#renderRawHeaders(rawHeadersText: string, forResponseHeaders: boolean): LitHtml.TemplateResult {
const trimmed = rawHeadersText.trim();
const showFull = forResponseHeaders ? this.#showResponseHeadersTextFull : this.#showRequestHeadersTextFull;
@@ -434,3 +583,118 @@
'devtools-request-headers-category': Category;
}
}
+
+interface HeaderDetailsDescriptor {
+ explanation: () => string;
+ examples: Array<{
+ codeSnippet: string,
+ comment?: (() => string),
+ }>;
+ link: {
+ url: string,
+ }|null;
+}
+
+interface HeaderDescriptor {
+ name: string;
+ value: Object|null;
+ headerValueIncorrect?: boolean|null;
+ details?: HeaderDetailsDescriptor;
+ headerNotSet: boolean|null;
+}
+
+const BlockedReasonDetails = new Map<Protocol.Network.BlockedReason, HeaderDescriptor>([
+ [
+ Protocol.Network.BlockedReason.CoepFrameResourceNeedsCoepHeader,
+ {
+ name: 'cross-origin-embedder-policy',
+ value: null,
+ headerValueIncorrect: null,
+ details: {
+ explanation: i18nLazyString(UIStrings.toEmbedThisFrameInYourDocument),
+ examples: [{codeSnippet: 'Cross-Origin-Embedder-Policy: require-corp', comment: undefined}],
+ link: {url: 'https://web.dev/coop-coep/'},
+ },
+ headerNotSet: null,
+ },
+ ],
+ [
+ Protocol.Network.BlockedReason.CorpNotSameOriginAfterDefaultedToSameOriginByCoep,
+ {
+ name: 'cross-origin-resource-policy',
+ value: null,
+ headerValueIncorrect: null,
+ details: {
+ explanation: i18nLazyString(UIStrings.toUseThisResourceFromADifferent),
+ examples: [
+ {
+ codeSnippet: 'Cross-Origin-Resource-Policy: same-site',
+ comment: i18nLazyString(UIStrings.chooseThisOptionIfTheResourceAnd),
+ },
+ {
+ codeSnippet: 'Cross-Origin-Resource-Policy: cross-origin',
+ comment: i18nLazyString(UIStrings.onlyChooseThisOptionIfAn),
+ },
+ ],
+ link: {url: 'https://web.dev/coop-coep/'},
+ },
+ headerNotSet: null,
+ },
+ ],
+ [
+ Protocol.Network.BlockedReason.CoopSandboxedIframeCannotNavigateToCoopPage,
+ {
+ name: 'cross-origin-opener-policy',
+ value: null,
+ headerValueIncorrect: false,
+ details: {
+ explanation: i18nLazyString(UIStrings.thisDocumentWasBlockedFrom),
+ examples: [],
+ link: {url: 'https://web.dev/coop-coep/'},
+ },
+ headerNotSet: null,
+ },
+ ],
+ [
+ Protocol.Network.BlockedReason.CorpNotSameSite,
+ {
+ name: 'cross-origin-resource-policy',
+ value: null,
+ headerValueIncorrect: true,
+ details: {
+ explanation: i18nLazyString(UIStrings.toUseThisResourceFromADifferentSite),
+ examples: [
+ {
+ codeSnippet: 'Cross-Origin-Resource-Policy: cross-origin',
+ comment: i18nLazyString(UIStrings.onlyChooseThisOptionIfAn),
+ },
+ ],
+ link: null,
+ },
+ headerNotSet: null,
+ },
+ ],
+ [
+ Protocol.Network.BlockedReason.CorpNotSameOrigin,
+ {
+ name: 'cross-origin-resource-policy',
+ value: null,
+ headerValueIncorrect: true,
+ details: {
+ explanation: i18nLazyString(UIStrings.toUseThisResourceFromADifferentOrigin),
+ examples: [
+ {
+ codeSnippet: 'Cross-Origin-Resource-Policy: same-site',
+ comment: i18nLazyString(UIStrings.chooseThisOptionIfTheResourceAnd),
+ },
+ {
+ codeSnippet: 'Cross-Origin-Resource-Policy: cross-origin',
+ comment: i18nLazyString(UIStrings.onlyChooseThisOptionIfAn),
+ },
+ ],
+ link: null,
+ },
+ headerNotSet: null,
+ },
+ ],
+]);