Angular 7 + dotnet core + SignalR IIS Issue - c#

I'm trying to deploy an Angular 7/.net Core application on my local IIS and am running into an issue. I used the Angular template in Visual Studio to create a .net core backend with an Angular front-end. I also added SignalR to both projects. Here are some code samples:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
services.AddSignalR();
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
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();
}
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseCors("CorsPolicy");
app.UseSignalR(routes =>
{
routes.MapHub<MyHub>("/myHub");
});
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.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
connection.service.ts
if (!this.hubConnection) {
this.hubConnection = new
HubConnectionBuilder().withUrl('http://localhost:5000/myhub').build();
}
public start(): void {
this.hubConnection
.start()
.then(() => {
console.log('Connection started');
this.startingSubject.next();
})
.catch((error: any) => this.startingSubject.error(error));
}
data.component.ts
private getAllData(): Promise<Data> {
const publishDate = this.getPublishDate();
return this.connectionService.hubConnection.invoke("GetAllData",
publishDate);
}
As a quick summary, I have a connection service to handle the signalR connections on the Angular side. Essentially, app.component.ts calls the Start() method in connection.service.ts which starts the SignalR connection. data.component.ts is subscribed to this event and when the connection is successful, it calls the GetAllData() method.
I was trying to follow this tutorial in getting this set up via IIS, but can't get it to work. (https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/?view=aspnetcore-2.2)
I Publish from Visual Studio, this creates a directory with my .net Core DLL and a ClientApp folder for my Angular site. If I do a dotnet myapp.dll command I can navigate to localhost:5000 and everything works great.
localhost:5000/myhub returns a response from my signalR hub
localhost:5000/client shows the signalR client webpage perfectly
localhost:5000/host shows the signalR host webpage perfectly.
I should also note that this works when running through VS too. However, when I run through IIS, I get these results:
localhost:5000/myhub returns a response from my signalR hub
localhost:5000/client shows the signalR client webpage perfectly
localhost:5000/host fails with:
ERROR Error: Uncaught (in promise): Error: An unexpected error
occurred invoking 'GetAllData' on the server. Error: An unexpected
error occurred invoking 'GetAllData' on the server.
/Host does try to make a call to /myhub, which makes me wonder if IIS has an issue with this communicating with the same port or something. or maybe I'm just setting up IIS wrong.
Does anyone have any ideas as to how to get this working via IIS? I'm been scratching my head over this all afternoon.
Edit:
After continuing to troubleshoot, it looks like the data.component.ts is successfully calling a "Connect" method on the hub just before the "GetAllData" method.
public Data GetAllData(DateTime? publishDate)
{
... Logic here
}
PublishDate should allow nulls (in this scenario, null is actually being passed to this method), is it possible this isn't allowed for some reason? Again, i have a hard time seeing why this would work everywhere but IIS, but I'm grasping at straws that this point. Seems weird that Connect() would work but GetAllData() wouldn't when they're on the same hub.
One more edit
The more I research, the more it looks like there is an actual exception within the GetAllData() method. I'm working at verifying this but I think what's happening is that I have a file path that I'm trying to access but this file path doesn't exist when the application is built. I'm not 100% sure as to why it's failing for IIS only but I'm continuing to dig. I'll post my findings in case anyone else stumbles across this very specific issue :)

I may have missed it.. but where is your MyHub class?
Something like this:
Public class MyHub : Hub {
Public async Task GetAllData() {
*logic here for when client calls hub*
}
}

Related

How can I create a .NET web server to receive URLs with arbitrarily long segments?

I'm working on a project that requires a web server to listen for GET requests where the URL is structured as /function/JWT where /function is the command and /JWT is a JSON web token. Arguments about the security of this practice aside, that's how the sender is providing the data.
I was able to build out some of the other requirements for this project using the System.Net.HttpServer class, but I'm finding now that it limits URL segments to something like 260 characters before it returns a 400 server error. I can't be changing registries every time I deploy this utility, so it seems like I might have to try something else.
How can I use .NET core to launch a web server that will detect when /function is the start of the absolute path and hand the JWT off to other code for interpretation? Some of the example tokens I have are over 500 characters long and they may be more in other cases so I need the max URL segment length to be much longer. Despite hours of research on this I can't find a simple answer to this seemingly simple problem.
Using a self-hosted ASP.NET Core app (i.e., not fronted by IIS or another reverse proxy with its own limit):
The MaxRequestLineSize for Kestrel defaults to 8192 bytes, and can be configured to a greater length if required:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(serverOptions =>
{
serverOptions.Limits.MaxRequestLineSize = 16000;
})
.UseStartup<Startup>();
});
}
And then you can simply configure an endpoint to handle the function/ route:
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/function/{jwt}", async context =>
{
var jwt = (string)context.Request.RouteValues["jwt"];
await context.Response.WriteAsync($"JWT is {jwt}, Length is {jwt.Length}");
});
});
}
}

Getting CORS error in ReactJS app when attempting to execute localhost API

I'm attempting to create a simple app to do some AES encryption (required for work). I created a small app in ReactJS that has a textbox for the plain text, a button that calls the encrypt method, and then a readonly textbox that displays the encrypted value.
The encrypt method calls an API that I wrote with ASP.NET Core (lives locally for now) to test the encryption out. The API successfully encrypts and returns the encrypted value when I execute in Postman and/or the browser but when I do it through my React app it throws the CORS error.
I'm extremely new at both creating API's and React so apologies in advance if the fix is a very simple one.
Here is my Startup class in the API project:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseCors(
options => options.WithOrigins("https://localhost:5001/encryption/encrypt").AllowAnyMethod()
);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
And then here is my encrypt method in the React app:
encrypt() {
var plainText = this.textToEncrypt.value;
console.log(plainText);
var requestOptions = {
method: 'GET',
headers: {
'Access-Control-Allow-Origin' : '*'
}
};
fetch("https://localhost:5001/encryption/encrypt?plainText=" + plainText, requestOptions)
.then(response => response.text())
.then(data => this.setState({ encryptedText: data.value }))
// .then(result => console.log(result))
.catch(error => console.log('error', error));
}
Here's the error from the browser console:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://localhost:5001/encryption/encrypt?plainText=sample%20text. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).
I don't know do we have the same CORS problem, but once I solved that problem with these changes:
Adding this line in package.json file:
"proxy": "http(s)://backend.url/api",
And/or by disabling host checking in .env file:
DANGEROUSLY_DISABLE_HOST_CHECK=true
This is not a perfect approach in that this absolutely is not how you want to address the issue in a production environment, but for development purposes you could try this.
if (env.IsDevelopment())
{
app.UseCors(
options => options.WithOrigins("*").AllowAnyMethod()
);
}
This would be in place of your current UseCors implementation. The problem with your current implementation is that (if properly syntaxed) would only allow CORS from itself. You need to allow CORS from the source of your node server (or whatever domain/port is running your web app). Since you do not indicate that in your post, * covers everything (very unsafe, as a reminder, for production).

Custom Middleware is causing Blazor Server-side page to stop working

My custom middleware appears to be causing some sort of conflict with my blazor server-side page. The sample middleware in short checks for a bool status and if true redirects to the counter page provided by the out of the box blazor template. Without the middleware inserted into the pipeline the counter page works just fine when you click the button, but once the middleware is placed in the pipeline the button no longer works, as it does not increment the count. I've placed the custom middleware right before the app.UseEndpoints middleware, though it appears it doesn't matter where it's placed, as it doesn't work no matter the order that it's in. Why is my custom middleware breaking the blazor server-side page from functioning properly?
middleware:
class CheckMaintenanceStatusMiddleware
{
private readonly RequestDelegate _next;
public CheckMaintenanceStatusMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var statusCheck = true;
if(statusCheck && context.Request.Path.Value != "/counter")
{
context.Response.Redirect("/counter");
}
else
{
await _next.Invoke(context);
}
}
}
public static class CheckMaintenanceStatusMiddlewareExtension
{
public static IApplicationBuilder UseCheckMaintenanceStatus(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CheckMaintenanceStatusMiddleware>();
}
}
configure method in Startup file:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var connectionString = Configuration.GetConnectionString("DefaultConnection");
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
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();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseCheckMaintenanceStatus();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
With your code , the blazor negotiate url is also been redirect so negotiate won't work .
Try below codes which avoid that :
if (statusCheck && context.Request.Path.Value != "/counter"&& !context.Request.Path.Value.StartsWith("/_blazor"))
{
context.Response.Redirect("/counter");
}
else
{
await _next.Invoke(context);
}
I suspect what's happening here is
The browser goes to the app URL and attempts to negotiate a SignalR connection.
SignalR returns the protocol and token to use to successfully connect
to the Hub (using "/_blazor?id=token_value").
Unfortunately the custom middleware is redirecting every request, so it ends up stopping the application from doing the initial connection to the hub (triggering a bunch of console errors) - though I was able to successfully redirect to "/counter". But because the middleware is stopping the SignalR connection, it breaks the connection from the client to the server which is necessary for Blazor Server apps.
I would suggest moving this check from middleware. You could try creating a service that can return if the app is in maintenance mode, add a service call to the Index.razor component, and then render a "Maintenance Mode" component if necessary.
This is old post.
But I am using app_offline.htm page to stop the blazor app
I put it in the root (content root folder) and have just plain html which says "Site is under maintenance"
This page will terminate all request and all blazor hub connection.
Then, when I am done updating my app, I rename this file to app_offline2.htm
Meaning "app_offline.htm" is a special file name used my framework and Core to determine if any request can be served by application.

ASP.NET Core WebAPI 500 Internal error in IIS 7.5

I'm struggling to get this WebAPI to work. Well, work with IIS. Everything works fine in IIS express, but when I publish it, specifically 1 api request doesn't work. I'm trying to access a url of API/[Controller]/{date}/{integer}. I keep getting the 500 server error. My other route of API/[Controller]/{date} works.
Here's my API Controller:
[Route("api/[controller]")]
public class PostingsController : Controller
{
// GET: api/Postings/5
[HttpGet("{date}")]
public string Get(string date)
{
return date;
}
// GET api/Postings/20160407/2
[HttpGet("{date}/{modeID}")]
public List<TruckPosting> Get(string date, int modeID)
{
TruckPosting tp = new TruckPosting();
List<TruckPosting> truckPostings = tp.GetTruckPostings(date, modeID);
return truckPostings;
}
}
Could the reason be that I'm trying to return a List<>? I'm stumped considering it works fine in VS's IIS Express.
Edit
Here's my startup.cs page:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure1(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseIISPlatformHandler();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseFileServer(true);
app.UseMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.Map("/appRoot", (app1) => this.Configure1(app1, env, loggerFactory));
}
That's a good thought that it might that fact that you're returning a List. We have working Core Web API methods and all of them return Task<IEnumerable<Foo>>. Try changing the return type List<TruckPosting> to Task<IEnumerable<TruckPosting>>
EDIT: To view the details for 500 (internal server) errors you will need to put the following line of code at the beginning of your Configure (or Configure1) method:
app.UseDeveloperExceptionPage();
And obviously this is not something you want in a production environment.
EDIT2: When running in VS you can use the code below to show exception details as long as the Hosting:Environment variable in the Debug section of Properties is set to "Development". After publishing you will need to create a System Environment variable named ASPNET_ENV and set its value to "Development" or the code will not call the UseDeveloperExceptionPage() method.
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
I posted an ASP.NET Core custom exception handler on GitHub today. I thought it might be helpful to you since you're building an SPA and are probably calling a lot Web API methods.
It's a middleware project that returns error messages for Web API calls and redirects to an error page for non Web API calls. It can also log the exceptions in a SQL Server database. That's done in a separate thread to help with performance.
The solution includes a demo application that shows how to use the exception handler for both Web API exceptions and non Web API exceptions.
Here's the link.
https://github.com/ClintBailiff/CustomExceptionHandler
If you end up checking it out and have some suggestions or comments, I like to hear them.

Using Tempdata is crashing my application

I'm very new to ASP.NET and am attempting to pass an object between two controllers in a web application I'm making in Visual Studio 2015. The web application is using an ASP.Net 5 Preview Template Web application (if it helps, I think I'm using beta code 7 and I'm not building for DNX Core 5).
The problem I'm having is whenever I try to put anything into the TempData variable, the program seems to crash. For example, in a "Create" method I have:
[HttpPost]
public ActionResult Create(Query query)
{
switch (query.QueryTypeID)
{
case 1:
TempData["Test"] = "Test";
return RedirectToAction("Index", "EventResults");
case 2:
break;
default:
break;
}
return View();
}
In that method, I attempt to add a simple test string under the key "test". When I run the application with that TempData statement in there, I receive an error message stating
An unhandled exception occurred while processing the request.
InvalidOperationException: Session has not been configured for this application >or request.
Microsoft.AspNet.Http.Internal.DefaultHttpContext.get_Session()
I have tried going to the Web.config located in the wwwroot element of the project and adding a "sessionState" object into a "system.web" element, but this had no effect on the error.
Any help would be very much so appreciated as I've been looking for solutions for this everywhere. I'm hoping it's something stupid/blindingly obvious that I somehow missed.
In order to use middleware, such as Session, Cache, etc in ASP.NET 5, you have to enable them explicitly.
Enabling session is done by adding the appropriate nuget package in your project.json file's dependencies section (make sure that the package version matches the versions of the other dependencies you have added):
"Microsoft.AspNet.Session": "1.0.0-*"
and the appropriate session (cache) storage package as well (like the example below; in memory):
"Microsoft.Extensions.Caching.Memory": "1.0.0-*"
and adding the middleware to dependency resolution in the Startup.cs Service configuration:
public void ConfigureServices(IServiceCollection services)
{
services.AddCaching();
services.AddSession(/* options go here */);
}
and adding the middleware to OWIN in the Startup.cs OWIN configuration:
public void Configure(IApplicationBuilder app)
{
app.UseSession();
//...
Make sure that the UseSession comes before the MVC configuration.
For Asp.Net Core, make sure Asp.NetCore.Session is added.
You can configure session in StartUp.cs like below.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<ITempDataProvider, CookieTempDataProvider>();
// Adds a default in-memory implementation of IDistributedCache.
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.CookieHttpOnly = true;
});
}
public void Configure(IApplicationBuilder app)
{
app.UseSession();
app.UseMvcWithDefaultRoute();
}

Categories