How can I generate absolute links to other resources in my RESTful API app when the app is meant to be accessed via a reverse proxy that publishes just the paths under /api?
My app is an API with a common layout of routes like /api, /swagger and /health. It is published on my employer's API management under a path of the form /business-area/api-name/v1. Calling the API both directly and through the API gateway overall works: calling https://api-gateway.company.com/business-area/api-name/v1/some-resource results in internal call to https://my-app.company.com/api/some-resource.
The issue is that the links in my app's responses point directly to the backend app (https://my-app.company.com/api/another-resource), not the the API gateway (https://api-gateway.company.com/business-area/api-name/v1/another-resource). They are generated using IUrlHelper.
I solved the domain by the ForwardedHeadersMiddleware and adding the X-Forwarded-Host by a policy on the API management. Sadly, we are allowed to use just extremely simple policies, so if we published the API using multiple gateways, the current solution would generate link to just a single one. But that is an issue to be solved somewhen later; now it works OK.
I could not get the path to work well. I tried changing the paths using a middleware as hinted in the ASP.NET Core behind proxy docs:
app.Use((context, next) =>
{
context.Request.PathBase = "/business-area/api-name/v1";
if (context.Request.Path.StartsWithSegments("/api", out var remainder))
{
context.Request.Path = remainder;
}
return next();
});
When I insert this middleware high in the pipeline, it breaks the routing, but if I insert it low enough, the routing works OK and only link generation is affected. But it seems that only PathBase change really affects link generation as the /api is still in the generated URI. I can see that the Path of the request object is really changed, though, so it is probably just that link generation uses the routing info directly, without passing through my middleware, which makes sense, but it rules out the middleware solution.
Is wrapping the standard IUrlHelper in my own implementation and postprocessing the URLs it returns a good way to go? I don't know how to go about that. I use the IUrlHelper from the ControllerBase.Url property and debugger tells it is actually an instance of Microsoft.AspNetCore.Mvc.Routing.EndpointRoutingUrlHelper. Doing the wrapping in every action seems wrong (repetitive, error-prone).
Changing the routing so that /api moves to the root is my last resort option as it mixes up the namespaces: technical endpoints like /health and /swagger would live among the actual resources of the API. Is there a reasonable way to avoid that while keeping the links working? This all seems like a pretty standard problem and I am surprised I cannot find how to solve it.
We use .NET 5 and we will migrate to .NET 6 as soon as it is out, if that makes any difference.
ASP.NET Core 2.1.1 offers several seemingly related extension methods for appBuilder:
UseStaticFiles from Microsoft.AspNetCore.StaticFiles
UseSpaStaticFiles from Microsoft.AspNetCore.SpaServices.Extensions
UseSpa from Microsoft.AspNetCore.SpaServices.Extensions
Please help me make sense of their purpose and relation to each other?
Also, is there any difference from the server execution standpoint if I run these methods in a different order
e.g.
app.UseStaticFiles() -> app.UseSpaStaticFiles() -> app.UseSpa()
vs
app.UseSpa() -> app.UseSpaStaticFiles() -> app.UseStaticFiles()
Static files, such as HTML, CSS, images, and JavaScript, are assets an
ASP.NET Core app serves directly to clients. Some configuration is
required to enable serving of these files.
UseStaticFiles - Serve files inside of web root (wwwroot folder)
UseSpaStaticFiles - Serve static file like image, css, js in asset
folder of angular app
UseSpa - let asp.net core know which directory you want to run your
angular app, what dist folder when running in production mode and
which command to run angular app in dev mode
Example
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
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");
}
});
UseStaticFiles serves files from wwwroot but it can be changed.
UseSpaStaticFiles does a similar thing but it requires ISpaStaticFileProvider to be registered. If app.ApplicationServices.GetService<ISpaStaticFileProvider>() returns null, then you will get an exception.
throw new InvalidOperationException($"To use {nameof(UseSpaStaticFiles)}, you must " +
$"first register an {nameof(ISpaStaticFileProvider)} in the service provider, typically " +
$"by calling services.{nameof(AddSpaStaticFiles)}.");
So you need to call app.AddSpaStaticFiles() to register default ISpaStaticFileProvider
UseSpa does two things. Rewrites all requests to the default page and tries to configure static files serving. On the contrary to UseSpaStaticFiles it does not throw an exception but just falls back to wwwroot folder.
Actually UseSpaStaticFiles and UseSpa both internally call the same method UseSpaStaticFilesInternal but with a different value for the 3rd parameter which is allowFallbackOnServingWebRootFiles. That is the reason why UseSpaStaticFiles throws an exception if no ISpaStaticFileProvider was registered it simply does not allow to fall back to wwwroot.
BTW if UseSpa falls back to wwwroot internally it calls old good app.UseStaticFiles(staticFileOptions);
Links to github sources:
SpaDefaultMiddleware
SpaStaticFilesExtensions
I've done a deep-dive into how this works recently and I thought I'd explain in detail here, because I don't think it's that well documented. I've investigated how it works with Visual Studio's React template, but it'll work similarly for other SPA frameworks too.
An important point to understand is that, perhaps unlike a more traditional ASP.NET web application: the way your ASP.NET SPA site runs in development is radically different to how it runs in production.
The above is very important to remember because much of the internal implementation of this stuff only applies in production, when you're not routing requests through to a Webpack development webserver. Most of the relevant ASP.NET SPA code is located at this location in the source repo, which I'll be referencing occasionally below. If you want to really dig into the implementation, look there.
IMPORTANT UPDATE:
With .NET 6, Microsoft have completely flipped how this works and now put the Webpack/Vite/whatever SPA dev server in front of the ASP.NET dev server. Frankly, this irrirtates me and I don't like what they've done. I was perfectly happy with the proxying method before. That code is still there, but presumably they will gradually stop supporting it. This sucks. Whatever. I'll leave the guide below, but it applies to the recommended .NET 5 approach, not the .NET 6 one.
How SPA works in ASP.NET
So, to go through the various SPA calls in the order in which they're called by default in the template:
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration => {
configuration.RootPath = "ClientApp/build";
});
The comment is slightly misleading, as I'll explain below. This is called in ConfigureServices, and adds in the DefaultSpaStaticFileProvider as an ISpaStaticFileProvider for DI. This is required if you're going to call UseSpaStaticFiles later on (which you probably are). RootPath is specified here (see later on for what it does). Next (remaining calls are made in Configure):
app.UseStaticFiles();
This is good old UseStaticFiles, tried-and-tested way of serving static files from the wwwroot directory. This is called before the others, meaning that if a requested path exists in wwwroot, it will be served immediately from there, and not looked for in the SPA directory (which is ClientApp/public by default during development, or ClientApp/build by default during production - see 'Where do the static assets get served from?' below). If it doesn't exist there, it will fall through to the next middleware, which is:
app.UseSpaStaticFiles();
This is similar to app.UseStaticFiles, but serves static files from a different directory from wwwroot - your 'SPA' directory, which defaults to ClientApp. However, although it can technically work during development, it is only intended to do anything during production. It looks in the above-mentioned RootPath directory - something like ClientApp/build - and tries to serve files from there if they exist. This directory will exist in a published production site, and will contain the SPA content copied from ClientApp/public, and also what was generated by Webpack. However, even though UseSpaStaticFiles is still registered when the site's running in development, it will probably fall through, because ClientApp/build doesn't exist during development. Why not? If you publish your app, the ClientApp/build directory will indeed be created under your project's root directory. But the SPA templates rely on it being deleted during development, because when you run app.UseSpa later on, it eventually runs npm run start, which (if you look in package.json) will run something like:
"start": "rimraf ./build && react-scripts start",
Notice the destruction of the build directory. UseSpaStaticFiles is relying on a npm script being triggered by a later piece of middleware to delete the build directory and effectively stop it from hijacking the pipeline during development! If you manually restore that build directory after starting the site, this middleware will serve files from it even during development. Rather Heath Robinson. As I mentioned above, the comment about React files being served from this directory 'in Production' is slightly misleading because, well, they'll be served from here during development too. It's just that there is an assumption that this directory won't exist during development. Why they didn't just put something like this in the template I'm not sure:
if (!env.IsDevelopment()) {
app.UseSpaStaticFiles();
}
And indeed I'd recommend you put that if clause in so you're not relying on build being deleted from the filesystem.
UPDATE: I've just noticed that this middleware isn't quite as odd as I'd first thought. The DefaultSpaStaticFileProvider actually checks to see whether ClientApp/build exists upon instantiation, and if it doesn't, it doesn't create a file provider, meaning that this will reliably fall through during development when ClientApp/build doesn't exist, even if you restore the directory later. Except that my above description of the behaviour still applies the first time you run the site after publishing, because it's still true that on the first run, ClientApp/build will exist, so this check is a bit of a questionable way of detecting whether static files should never be served (like in a dev environment proxying through to an internal Webpack dev server) or not. I still think my above wrapping of UseSpaStaticFiles in a clause along the lines of if (!env.IsDevelopment()) { ... } is a more reliable and simpler way to do it, and I'm puzzled that they didn't take that approach.
But anyway, this firmware is intended to fall through 100% of the time during development, because the directory should be deleted when the request is made to ASP.NET, to the final middleware:
app.UseSpa(spa => {
//spa.Options.SourcePath = "ClientApp";
// ^ as this is only used in development, it's misleading to put it here. I've moved
// it inside the following 'if' clause.
if (env.IsDevelopment()) {
spa.Options.SourcePath = "ClientApp";
// This is called for the React SPA template, but beneath the surface it, like all
// similar development server proxies, calls
// 'SpaProxyingExtensions.UseProxyToSpaDevelopmentServer', which causes all
// requests to be routed through to a Webpack development server for React, once
// it's configured and started that server.
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
This 'catch-all' middleware rewrites all requests to the 'default page' for the SPA framework you're using, which is determined by spa.Options.DefaultPage, which defaults to /index.html. So all requests will by default render /index.html during production, allowing the client app to always load its page and deal appropriately with requests to different URLs.
However, this middleware is NOT intended to get hit during development, because of what's inside the above if (env.IsDevelopment()) { ... } clause. That UseReactDevelopmentServer (or whatever eventually calls UseProxyToSpaDevelopmentServer, or maybe a direct call to UseProxyToSpaDevelopmentServer) adds a terminal middleware that proxies all requests to a Webpack development server, and actually prevents requests from going through to the UseSpa middleware. So, during production, this middleware runs and acts as a "catch-all" to render index.html. But during development, it is not indended to run at all, and requests should be intercepted first by a proxying middleware that forwards to a Webpack development server and returns its responses back. The Webpack development server is started in the working directory specified by spa.Options.SourcePath, so it will serve ClientApp/public/index.html as the catch-all webpage during development (/public/??? See below.) The spa.Options.SourcePath option is not used during production.
Where do the static assets get served from?
Take a given request for a file /logo.png. wwwroot will first be checked, and if it exists there, it will be served. During production, ClientApp/build will then be checked for the file, as that was the path configured in services.AddSpaStaticFiles. During development, however, this path is effectively not checked (it is, but it should always have been deleted before development starts; see above), and the path that will get checked for static assets instead is ClientApp/public in your project's root directory. This is because the request will be proxied through to an internal Webpack development server. The Webpack development server serves both dynamically-generated Webpack assets, but also static assets like /logo.png, from its "static directory" option, which defaults to public. Since the server was started in the ClientApp working directory (thanks to the spa.Options.SourcePath option), it will try to serve static assets from ClientApp/public.
In summary - execution flow
Basically, ASP.NET's SPA methods are trying to roughly emulate at production what the Webpack development server does at development - serve static assets first (although ASP.NET also tries wwwroot first, which the Webpack dev server obviously doesn't do), then fall through to a default index.html for the SPA app (the Webpack dev server doesn't do this by default, but things like react-scripts have a default setup which adds a plugin causing this to happen).
However, at development, ASP.NET actually does proxy requests through to that Webpack dev server, so its SPA middleware is basically meant to get bypassed. Here's a summary of the intended flow in both cases:
Production:
Request
-> Check in /wwwroot
-> Check in /ClientApp/build
-> Rewrite request path to /index.html and serve that from /ClientApp/build
Development:
Request
-> Check in /wwwroot
-> Proxy through to internal Webpack dev web server, which:
-> ... serves static assets from /ClientApp/public
-> ... serves dynamically-generated Webpack assets from their locations
-> ... failing that, rewrites request path to /index.html and serves that from /ClientApp/public
An additional WTF
Microsoft made a few design decisions I think are a bit questionable with how they did this, but one behaviour makes no sense to me and I have no idea of its use-case, but it's worth mentioning.
app.UseSpa ends up calling app.UseSpaStaticFilesInternal with the allowFallbackOnServingWebRootFiles option set to true. The only time that has an effect is if you didn't previously add an ISpaStaticFileProvider to DI (eg. by calling services.AddSpaStaticFiles), and you call app.UseSpa anyway. In that case, instead of throwing an exception, it will serve files from wwwroot. I honestly have no idea what the point of this is. The template already calls app.UseStaticFiles before anything else, so files from wwwroot already get served as top priority. If you remove that, and remove services.AddSpaStaticFiles, and don't call app.UseSpaStaticFiles (because that'll throw the exception), then app.UseSpa will serve files from wwwroot. If that has a use-case, I don't know what it is.
Further thoughts
This setup works OK, but the 'on-demand' setup of the Webpack development server seems to spin up a ton of node instances, which seems rather inefficient to me. As suggested under 'Updated development setup' in this blog (which also provides some good insights into how this SPA stuff works), it might be a better idea to manually run the Webpack development server (or Angular/React/whatever) at the beginning of your development session, and change the on-demand creation of a dev server (spa.UseReactDevelopmentServer(npmScript: "start")) to on-demand creation of a proxy to the existing dev server (spa.UseProxyToSpaDevelopmentServer("http://localhost:4200")), which should avoid spinning up 101 node instances. This also avoids some unnecessary slowness of rebuilding the JS each time something in the ASP.NET source changes.
Hmm... MS went to so much effort to allow that proxying through to a node-backed Webpack development web server, it would almost be simpler if they just recommended that in production, you deploy a node-based web server to proxy all SPA requests through to. That would avoid all the extra code which acts differently in production and development. Oh well, I guess that this way, at least when you get to production, you're not reliant on node. But you pretty much are at development. Probably unavoidable given how all the SPA ecosystems are designed to work with node. When it comes to SPA applications, node has effectively become a necessary build tool, akin to the likes of gcc. You don't need it to run the compiled application, but you sure need it to transpile it from source. I guess hand-crafting the JavaScript to run in the browser, in this analogy, would be like hand-writing the assembly. Technically possible, but not really done.
On an intranet site using windows authentication, and certain controller methods being marked with the "AuthorizeAttribute" controlling access to certain users/groups and roles, I'm trying to figure out the best way to allow "test users" to access these things.
Since <location> is off the table with MVC (security concerns), what is the best approach here?
My first thought is to implement the following:
A custom config section that essentially mirrors the <authorization> section
A custom attribute that inherits from "AuthorizeAttribute" which checks users against the custom config section
Use config transforms to remove the custom config section for QA and Release environments
Is there an easier/better way???
Update
What I originally wrote used the attribute syntax on a class or method, but if you are using MVC3 you can also use a global action filter in (global.asax.cs) so you only have to do it once.
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
#if DEBUG
filters.Add(new AuthorizeAttribute() {Users="YourAccount"});
#endif
//Your other global action filters
}
Original
You could use #if DEBUG to only add the authorization to debug code.
#if DEBUG
[Authorize(Users = "YourAccount")]
#endif
The Authorize attribute allows multiple so you don't have to repeat your production authorized user list or use an #else.
I am trying to build a webservice that manipulates http requests POST and GET.
Here is a sample:
public class CodebookHttpHandler: IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
if (context.Request.HttpMethod == "POST")
{
//DoHttpPostLogic();
}
else if (context.Request.HttpMethod == "GET")
{
//DoHttpGetLogic();
}
}
...
public void DoHttpPostLogic()
{
...
}
public void DoHttpGetLogic()
{
...
}
I need to deploy this but I am struggling how to start. Most online references show making a website, but really, all I want to do is respond when an HttpPost is sent. I don't know what to put in the website, just want that code to run.
Edit:
I am following this site as its exactly what I'm trying to do.
I have the website set up, I have the code for the handler in a .cs file, i have edited the web.config to add the handler for the file extension I need. Now I am at the step 3 where you tell IIS about this extension and map it to ASP.NET. Also I am using IIS 7 so interface is slightly different than the screenshots. This is the problem I have:
1) Go to website
2) Go to handler mappings
3) Go Add Script Map
4) request path - put the extension I want to handle
5) Executable- it seems i am told to set aspnet_isapi.dll here. Maybe this is incorrect?
6) Give name
7) Hit OK button:
Add Script Map
Do you want to allow this ISAPI extension? Click "Yes" to add the extension with an "Allowed" entry to the ISAPI and CGI Restrictions list or to update an existing extension entry to "Allowed" in the ISAPI and CGI Restrictions list.
Yes No Cancel
8) Hit Yes
Add Script Map
The specified module required by this handler is not in the modules list. If you are adding a script map handler mapping, the IsapiModule or the CgiModule must be in the modules list.
OK
edit 2: Have just figured out that that managed handler had something to do with handlers witten in managed code, script map was to help configuring an executable and module mapping to work with http Modules. So I should be using option 1 - Add Managed Handler.
I know what my request path is for the file extension... and I know name (can call it whatever I like), so it must be the Type field I am struggling with. In the applications folder (in IIS) so far I just have the MyHandler.cs and web.config (Of course also a file with the extension I am trying to create the handler for!)
edit3: progress
So now I have the code and the web.config set up I test to see If I can browse to the filename.CustomExtension file:
HTTP Error 404.3 - Not Found
The page you are requesting cannot be served because of the extension configuration. If the page is a script, add a handler. If the file should be downloaded, add a MIME map.
So in IIS7 I go to Handler Mappings and add it in. See this MSDN example, it is exactly what I am trying to follow
The class looks like this:
using System.Web;
namespace HandlerAttempt2
{
public class MyHandler : IHttpHandler
{
public MyHandler()
{
//TODO: Add constructor logic here
}
public void ProcessRequest(HttpContext context)
{
var objResponse = context.Response;
objResponse.Write("<html><body><h1>It just worked");
objResponse.Write("</body></html>");
}
public bool IsReusable
{
get
{
return true;
}
}
}
}
I add the Handler in as follows:
Request path: *.whatever
Type: MyHandler (class name - this appears correct as per example!)
Name: whatever
Try to browse to the custom file again (this is in app pool as Integrated):
HTTP Error 500.21 - Internal Server Error
Handler "whatever" has a bad module "ManagedPipelineHandler" in its module list
Try to browse to the custom file again (this is in app pool as CLASSIC):
HTTP Error 404.17 - Not Found
The requested content appears to be script and will not be served by the static file handler.
Direct Questions
1) Does the website need to be in CLASSIC or INTEGRATED mode? I don't find any reference of this in the online material, whether it should be either.
2) Do I have to compile the MyHandler.cs to a .dll, or can I just leave it as .cs? Does it need to be in a bin folder, or just anywhere in root?
RE your questions:
I don't know the answer to the first one (CLASSIC or INTEGRATED); but I can help with the second...
Yes you'll need to compile it first. I have never tried deploying dll's to anywhere other than the bin, given that that's the standard I would be suspect in putting them anywhere else even if it did work.
The way I deploy HttpHandlers is quiet straight forward - all the hard work's done in web.config, I'v enever had to go into IIS to change any settings.
For a start, for the http request to be handled by ASP.NET you need to use a request suffix that's already piped to ASP.NET - like .aspx or ashx. If you want to use something else you will need to config IIS to do this, as per your managed handler img above.
I tend to use .ashx e.g: http://localhost/foo/my/httphandler/does/this.ashx
All you need to do (assuming you've compiled athe HttpHandler into a DLL and deployed it to the site) is add the necessary config.
<configuration>
<system.web>
<httpHandlers>
<add verb="*"
path="*.ashx"
type="MyApp.PublishingSystem.HttpHandlers.GroovyHandler, MyApp.PublishingSystem" />
</httpHandlers>
</system.web>
</configuration>
Obviously (?) you can change / restrict the scope using the path, e.g:
path="*.ashx"
path="*ListWidgets.ashx"
path="*Admin/ListWidgets.ashx"
More info here: http://msdn.microsoft.com/en-us/library/ms820032.aspx
An important gotcha to look out for is the order in which you declare your HttpHandlers in the config; from what I remember ones declared first take precedent. So in this example...
<add verb="*" path="*foo.ashx" type="MyApp.PublishingSystem.HttpHandlers.FooHandler, MyApp.PublishingSystem" />
<add verb="*" path="*.ashx" type="MyApp.PublishingSystem.HttpHandlers.GroovyHandler, MyApp.PublishingSystem" />
...the groovy handler will handle all HttpRequests except any that end in foo.ashx
By the way, I make use of HttpHanldrs in my open source .net CMS / app framework, you might find some helpful code there (?): http://morphfolia.codeplex.com/
Make sure the app pool's .NET Framework Version is set correctly...
I deployed a .NET 4.0 web app on a .NET 2.0 app pool and got this error. Set the app pool to v4.X and the ashx was served like a champ.
I am using Context.RewritePath() in ASP.NET 3.5 application running on IIS7.
I am doing it in application BeginRequest event and everything works file.
Requests for /sports are correctly rewritten to default.aspx?id=1, and so on.
The problem is that in my IIS log I see GET requests for /Default.aspx?id=1 and not for /sports.
This kind of code worked perfectly under IIS6.
Using Microsoft Rewrite module is not an option, due to some business logic which has to be implemented.
Thanks.
EDIT:
It seems my handler is too early in the pipeline, but if I move the logic to a later event, than the whole rewrite thing doesn't work (it's too late, StaticFileHandler picks up my request).
I googled and googled, asked around, can't believe that nobody has this problem?
EDIT:
Yikes! Here's what I found on the IIS forum:
"This is because in integrated mode, IIS and asp.net share a common pipeline and the RewritePath is now seen by IIS, while in IIS6, it was not even seen by IIS - you can workaround this by using classic mode which would behave like IIS6."
Final update: Please take a look at my answer below, I've updated it with results after more than a year in production environment.
After some research, I've finally found a solution to the problem.
I have replaced the calls to Context.RewritePath() method with the new (introduced in ASP.NET 3.5) Context.Server.TransferRequest() method.
It seems obvious now, but not event Senior Dev Engineer on IIS Core team thought of that.
I've tested it for session, authentication, postback, querystring, ... issues and found none.
Tommorow I'll deploy the change to a very hight traffic site, and we'll soon know how it actually works. :)
I'll be back with the update.
The update: the solution is still not entirely on my production servers but it's tested and it does work and as far as I can tell so far, it's a solution to my problem. If I discover anything else in production, I will post an update.
The final update: I have this solution in production for over a year and it has proven to be a good and stable solution without any problems.
You could set the path back to the original value after the request has been processed but before the IIS logging module writes the log entry.
For example, this module rewrites the path on BeginRequest and then sets it back to the original value on EndRequest. When this module is used the original path appears in the IIS log file:
public class RewriteModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.BeginRequest += OnBeginRequest;
context.EndRequest += OnEndRequest;
}
static void OnBeginRequest(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
app.Context.Items["OriginalPath"] = app.Context.Request.Path;
app.Context.RewritePath("Default.aspx?id=1");
}
static void OnEndRequest(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
var originalPath = app.Context.Items["OriginalPath"] as string;
if (originalPath != null)
{
app.Context.RewritePath(originalPath);
}
}
public void Dispose()
{
}
}
I've had exactly the same problem. One way around this is to use Server.Transfer instead of Context.RewritePath. Server.Transfer doesn't restart the entire page lifecycle so the original URL will still be logged. Be sure to pass "true" for the "preserveForm" parameter so that the QueryString and Form collections are available to the 2nd page.
Old question, but I found I did not encounter your problem when I did the following:
a) A rewrite rule in web.config to direct all requests to /default.aspx, eg:
<rule name="all" patternSyntax="Wildcard" stopProcessing="true">
<match url="*"/>
<action type="Rewrite" url="/default.aspx"/>
</rule>
b) Called RewritePath in the Page_PreInit event of default.aspx, to rewrite the URL and querystring as what was passed in the request (ie. the location that doesn't exist).
For example, I request "/somepage/?x=y" (which doesn't exist).
a) Web.config rule maps it to /default.aspx
b) Page_PreInit rewrites it back to "/somepage/?x=y".
The result of this, in IIS 7 (Express and production) is that the server log reflects "/somepage" for stub and "x=y" for query, and all the Request object properties reflect the requested (non-existent) URL (which is what I wanted).
The only weird effect is, in IIS Express, the log item is written twice. However this doesn't happen in production (Windows Server 2008 R2).