Angular18+.NET8部署IIS10遇net::ERR_HTTP2_PROTOCOL_ERROR 200问题求助
部署在AWS环境下IIS 10.0.17763.1(Windows Server 2019 版本1809)的Angular 18 + .NET 8应用,已实现CSP Nonce机制。在Visual Studio 2022的IIS Express中运行正常,但在目标IIS环境返回错误:GET https://[my domain].[my sandbox].com/ net::ERR_HTTP2_PROTOCOL_ERROR 200 (OK),Angular应用完全无法启动加载视图,但Nonce已正确注入index.html。
相关代码实现
CspNonceMiddleware核心代码
public async Task InvokeAsync(HttpContext context) { // Generate the nonce string nonce = GenerateNonce(); context.Items["csp-nonce"] = nonce; // Only process HTML responses context.Response.OnStarting(() => { if (context.Response.ContentType?.Contains("text/html") == true) { // Remove existing CSP header if present if (context.Response.Headers.ContainsKey("Content-Security-Policy")) { context.Response.Headers.Remove("Content-Security-Policy"); } // Add CSP header with the nonce context.Response.Headers.Append("Content-Security-Policy", "default-src 'self'; base-uri 'self'; form-action 'self'; img-src 'self' data:; font-src 'self' data:; connect-src https: http: data: wss: ws:; frame-src https:; script-src-elem 'self' 'nonce-{nonce}'; script-src 'self' 'nonce-{nonce}'; style-src-elem 'self' 'nonce-{nonce}'; style-src 'self' 'nonce-{nonce}';"); } return Task.CompletedTask; }); // Intercept the response to inject the nonce into index.html var originalBodyStream = context.Response.Body; using (var newBodyStream = new MemoryStream()) { context.Response.Body = newBodyStream; // Call the next middleware await _next(context); // Reset the stream position to read it newBodyStream.Seek(0, SeekOrigin.Begin); string responseBody = new StreamReader(newBodyStream).ReadToEnd(); // Replace the placeholder in index.html with the nonce responseBody = responseBody.Replace("{nonce}", nonce); // Write the updated response back context.Response.Body = originalBodyStream; await context.Response.WriteAsync(responseBody); } } private string GenerateNonce() { var bytes = new byte[16]; RandomNumberGenerator.Fill(bytes); // Generate a secure random nonce return Convert.ToBase64String(bytes); }
main.ts中CSP_NONCE配置
export function getCspNonce(): string | null { const meta = document.querySelector('meta[name="csp-nonce"]'); return meta ? meta.getAttribute('content') : null; } const providers: Array<StaticProvider> = [ { provide: 'BASE_URL', useFactory: getBaseUrl, deps: [] }, { provide: 'API_URL', useFactory: getApiUrl, deps: [] }, { provide: 'COPYRIGHT_FULL', useFactory: getCopyRightFull, deps: [] }, { provide: 'COPYRIGHT', useValue: legalJson.copyright, deps: [] }, { provide: 'UI_VERSION', useValue: packageJson.version, deps: [] }, { provide: CSP_NONCE, useFactory: getCspNonce } ]; console.log(`Checking API server...`); const apiurl = getApiUrl() + "test" + "?fromUI=true"; fetch(apiurl) .then(response => { if (!response.ok) { // Handle HTTP error responses [Checks for servers, edited out for brevity...] } console.log(`API server is available.`); // Continue with normal execution if the response is okay platformBrowserDynamic(providers) .bootstrapModule(AppModule) .catch(err => console.error(err)); }) .catch(error => { // Handle network errors or errors thrown from the response status checks const img = document.getElementsByClassName('logo-img')[0]; img?.remove(); const h3 = document.getElementsByClassName('loading-text')[0]; h3.innerHTML = error.message || "An error occurred while connecting to the API server."; });
Startup.Configure方法
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.EnvironmentName.Equals("Development")) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } [Tried app.UseMiddleware here...] app.UseHttpsRedirection(); [Tried app.UseMiddleware here...] app.UseStaticFiles(); [Tried app.UseMiddleware here...] // Turned off browser check on 11/18/2023 //app.UseSupportedBrowserMiddleware(); app.UseSpaStaticFiles(); [Tried app.UseMiddleware here...] app.UseMiddleware<CspNonceMiddleware>(); app.UseSpa(spa => { // To learn more about options for serving an Angular SPA from ASP.NET Core, // see https://go.microsoft.com/fwlink/?linkid=864501 spa.Options.SourcePath = "ClientApp"; if (env.EnvironmentName.Equals("Development")) { spa.Options.StartupTimeout = new TimeSpan(0, 0, 480); spa.UseAngularCliServer(npmScript: "start"); } }); }
index.html文件
<!doctype html> <html lang="en"> <head> <!--x out localrefs and un-x server refs as needed to speed up dev. Server refs are faster and can be used if not debugging api or auth--> <title>MHK MarketProminence DEV</title> <base apiref="https://localhost:44378/" authref="https://localhost:44322/" href="/"> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Expires" content="0" /> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="csp-nonce" content=""/> <meta http-equiv="Content-Security-Policy" content="default-src 'self' ; base-uri 'self' ; form-action 'self' ; img-src 'self' data:; font-src 'self' data:; connect-src https: http: data: wss: ws:; frame-src https:; script-src-elem 'self' 'nonce-{nonce}' ; script-src 'self' 'nonce-{nonce}' ; style-src-elem 'self' 'nonce-{nonce}' ; style-src 'self' 'nonce-{nonce}' ;" /> <link rel="icon" type="image/x-icon" href="data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAdCAYAAAC9pNwMAAAD0klEQVR42rXWf0iTeRzA8VVqEB2VXD+g4Ojujy76Z7Z/cobtyoPKze5uLjGvWFRz5SKtOEU579nOIGGbbO2HzyzHptP2UG2mzUJz6UCmFDGjgoFBXBIUQlw7gmP2vc8TZ/f03WfpORy8/9r3eV58nt8i0Ry/gi3NX+wSW15B01SPRYv5+y7PugsQklKe5eaiwjKx5Xwa+FfhOqcBGQBF0HO37yk9uFvzXo5vMffy7pCLzt+LcS/3Ch42+xOe/fwwrv5kMK9xRdhcLdz5ziVtbrgn9AU8IadIEH/Gs2o6+KCnl7GUCErrzE7Wqo6ils0AVJaj29GX/OlCvajRhcIXefqD8dOIfCVcHGjGFAhjH40H63GJCrGFyvC+zL7CMenliA/InACbi4cuCQTiLwzG/aG+sygn8uubIFkOmUSty3mRout+F08DVA01QP/4/xD6b/GOriceyKAAAAAElFTkSuQmCC"> <style type="text/css" nonce="{nonce}"> body { background-color: #e9ebee; /*background-color: transparent1; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABQYAAAKfCAIAAACzD4L/AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvDQ0NDQ0OvlCYkHhoaGhoaGhoaGhoaGnqlNCHx0NDQ0NDQ0NDQ0NDQ0CulCYmHhoaGhoaGhoaGhoaGXilNSDw0NDQ0NDQ0NDQ0NDT0Kum///3/AVhVzNadZFJsAAAAAElFTkSuQmCC'); [Edited for brevity...] .logo-img { width: 128px; height: 128px; } </style> </head> <body> <app-root ngCspNonce="{nonce}"> <div id="pre-bootstrap"> <div class="messaging"> <img class="mp-login-img" alt="MHK MarketProminence Logo" src="data: image/png; base64, /h0Jb39y87b+4t4ckcGtq1LQX57qhFAqGlMhzVVVVX+XO9jmihl2XS5ExO8P5UDMTZ3AnIUKEOBRQ1GvG8oGnzlQqqXzprFNn4TmXe5kVQyvaa86vWq5UUPkSphjJnwcvMG74H+5tweTLb194HRtVqRVVPgT3E8esn829LLipfefhpIweumnZI0ollQ9hVAZauWh7nHt7yMIwqo/ScHOtQoEZ3K91Jqq8pUJ+/kQllDw+f1WM1J0Zhdh983vJ1bQX4up07CJPpOor5lqycuCgr+P4T7oK92RlO0AAAAAElFTkSuQmCC" /> <br /> <br /> <h3 class="loading-text"> Loading, please stand by... </h3> <br /> <img class="logo-img" alt="loading..." src="data:image/gif;base64,R0lGODlhMAEsAQAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBAAAZACwAAAAAMAEsAYQAAACtos+vpc+upc2upM+wptGvpM+qn8qro8uto8+upc+vpdCfj7+vpNCso8+nn8etpM+OcaqLdLmHgLiGebmAgLiLfruAQICOfbUAAAAAAAAAAAAAAAAAAAAAAAAAAAAF/2AmjmRpnmiqrmzrvnAsz3Rt33iu7FDYCJZvB0TonxGX/9TE9d0KjhXAtJWHoyU9AlkxheYhdaFU4hKLPY5J4lJc+U2JZRZCXx3/xoW6WVFx05XwEpII0C0Iu9VR2ZDZs9l9mQ6LLUJ1k6TTf6IR4u3WR3cmZRXVAWcihBamFDUdd58twQTqnCASmWEl1AAAA7" /> </div> </div> </app-root> </body> </html>
解决方案与排查建议
1. 修复CSP Header中的Nonce占位符替换问题
当前CspNonceMiddleware添加CSP Header时,'nonce-{nonce}'是未替换的字符串字面量,导致浏览器收到的CSP规则无效,触发资源拦截进而引发协议错误。修改代码:
// 替换原Header赋值代码 string cspPolicy = $"default-src 'self'; base-uri 'self'; form-action 'self'; img-src 'self' data:; font-src 'self' data:; connect-src https: http: data: wss: ws:; frame-src https:; script-src-elem 'self' 'nonce-{nonce}'; script-src 'self' 'nonce-{nonce}'; style-src-elem 'self' 'nonce-{nonce}'; style-src 'self' 'nonce-{nonce}';"; context.Response.Headers.Append("Content-Security-Policy", cspPolicy);
2. 调整中间件注册顺序
将UseMiddleware<CspNonceMiddleware>移至UseStaticFiles和UseSpaStaticFiles之前,确保中间件能优先拦截并修改SPA的index.html响应:
app.UseHttpsRedirection(); // 移至此处 app.UseMiddleware<CspNonceMiddleware>(); app.UseStaticFiles(); app.UseSpaStaticFiles();
3. 验证响应内容长度处理
中间件修改响应体后未更新Content-Length头,可能导致HTTP/2协议解析错误。添加代码更新长度或移除该头:
// 在WriteAsync后添加更新长度 context.Response.ContentLength = Encoding.UTF8.GetByteCount(responseBody); // 或移除头让IIS自动处理分块传输 context.Response.Headers.Remove("Content-Length");
4. 排查IIS HTTP/2兼容性
HTTP/2与响应流修改可能存在兼容性问题,可临时禁用HTTP/2测试:
- 打开IIS管理器,选择站点 -> 功能视图 -> 绑定
- 选择HTTPS绑定,点击"编辑"
- 取消勾选"启用HTTP/2"
若禁用后问题消失,需排查AWS负载均衡器或IIS的HTTP/2配置冲突。
5. 移除重复CSP配置
index.html中同时存在meta标签和HTTP头的CSP规则,浏览器会合并取更严格规则,建议移除index.html中的meta标签CSP,仅保留HTTP头方式。
6. 检查AWS环境拦截规则
确认AWS WAF、CloudFront等服务未修改或拦截响应头/内容,同时验证SSL证书配置完整,无证书链问题导致的HTTP/2握手失败。
内容的提问来源于stack exchange,提问作者JackDogStar

