TL;DR
IIS Rewrite (ALL) URIs with Trailing Slash
& preserve Fragment
and Query Strings
<rule name="AddTrailingSlash" stopProcessing="true">
<match url="^([^/]+:\/\/[^/#?]+|[^?#]+?)\/?((?:[^/?#]+\.[^/?#]+)?(?:[?#].*)?$)" />
<action type="Redirect" url="{R:1}/{R:2}" redirectType="Permanent" />
</rule>
IIS use ECMAScript so you can Try it here : https://regexr.com/6ele7
Update
IIS Rewrite (Considered) URIs with Trailing Slash
& preserve Fragment
and Query Strings
<rule name="AddTrailingSlash" stopProcessing="true">
<match url="^([^/]+:\/\/[^/#?]+|[^?#]+\/[^/.?#]+)([?#].*)?$" />
<action type="Redirect" url="{R:1}/{R:2}" redirectType="Permanent" />
</rule>
Try it here : https://regexr.com/6fk3g
http://127.0.0.1 --> http://127.0.0.1/
https://localhost --> https://localhost/
https://localhost? --> https://localhost/?
https://localhost/ --> https://localhost/
https://my.site.com --> https://my.site.com/
https://my.site.com:443? --> https://my.site.com:443/?
https://my.site.com/ --> https://my.site.com/
https://my.site.com/about.php --> https://my.site.com/about.php
https://my.site.com/about.php? --> https://my.site.com/about.php?
https://my.site.com/about --> https://my.site.com/about/
https://my.site.com/about? --> https://my.site.com/about/?
https://my.site.com/about/ --> https://my.site.com/about/
https://my.site.com/about/? --> https://my.site.com/about/?
https://my.site.com/about?query --> https://my.site.com/about/?query
https://my.site.com/about/?query --> https://my.site.com/about/?query
https://my.site.com/about.php?query --> https://my.site.com/about.php?query
https://my.site.com/about#hash --> https://my.site.com/about/#hash
https://my.site.com/about/#hash --> https://my.site.com/about/#hash
https://my.site.com/about.php#hash --> https://my.site.com/about.php#hash
https://my.site.com/about?query#hash --> https://my.site.com/about/?query#hash
https://my.site.com/about/?query#hash --> https://my.site.com/about/?query#hash
https://my.site.com/folder.name/about?query --> https://my.site.com/folder.name/about/?query
https://my.site.com/about?query#hash:http://test.com?q --> https://my.site.com/about/?query#hash:http://test.com?q
Explaination (All)
- Level 1 - Lets just think about your examples:
^([^?#]+?)\/?([?#].*)?$
Group #1: ^
In first, [^?#]
Any character except ?
/#
, Go much but lazy +?
(Stop on first possible, by looking to next)
Ignore: \/?
Then if a /
exist or not
Group #2: [?#]
= ?
/#
And .*
Any much character next to that till $
End, (...)?
If exist
It work well. But it will deal not right with:
https://my.site.com/about.php?query --> https://my.site.com/about.php/?query !!!
So let's add an exception...
- Level 2 - How if we take possible file name
Name.name.name.ext
as Group #2?
^([^?#]+?)\/?((?:[^/?#]+\.[^/?#]+)?(?:[?#].*)?)$
(?:...)
Non-Capturing group
([^/?#]+\.[^/?#]+)?
Look for any possible file name or (?:[?#].*)?
Any possible query or anchor strings
Now everything is OK, except this:
https://my.site.com? --> https://my.site.com? !!!
So we need another exception in Group #1
- Level 3 - Take just domain URI as an alternative
^([^/]+:\/\/[^/#?]+|[^?#]+?)\/?((?:[^/?#]+\.[^/?#]+)?(?:[?#].*)?$)
(...|...)
Alternative
[^/]+:\/\/[^/#?]+
First check if (not lazy) any pattern like ...://...
till not / # ?
exist?
Now it work great!
+ Explaination (Considered)
- Level 4 - How if we just add a Not-Accepting
.
& /
character set in first group to just match considered URIs and ignore others?
^([^/]+:\/\/[^/#?]+|[^?#]+\/[^/.?#]+)([?#].*)?$
\/[^/.?#]+
Check if after last /
the set of characters be not /.?#
Now it is even smaller and faster!
Analyzing other method
As @károly-szabó answered well here, instead of looking for Not-Accepted character sets, we can look for matched pattern.
So if we want to use the method but in simpler way (2 Groups) (+ Some minor optimization), the regex will be:
^(https?:\/\/[\w.:-]+\/?(?:[\w.-]+\/)*[\w-]+(?!\/))([?#].*)?$
But URI path Accepted characters are more.
So a wider version of that Regex can be:
^(https?:\/\/[\w.:-]+\/?(?:[\w!#-)+-.;=@~]+\/)*[\w!#-);=@~+,-]+(?!\/))([?#].*)?$
Try it here: https://regexr.com/6elea
Note: Still "multibyte Unicode as domain name is allowed" but i ignored that in this method.
P.S.
Actually i don't think that we should rewrite it on IIS, because of these reasons:
- Anchors char
#
can be part of a folder name (by %23
)
- A file name can have no extension
- IIS/Browsers usually will (/should) handle Anchors/Queries
Ref:
I Mean:
https://my.site.com/ --> (=Call root)
https://my.site.com/about --> (=Call root > Folder/File name about)
https://my.site.com/about/ --> (=Call root > Folder name about)
https://my.site.com/about?query --> (=Call root > Folder/File name about + Query)
https://my.site.com/about/?query --> (=Call root > Folder name about + Query)
https://my.site.com/about.php?query --> (=Call root > File name about.php + Query)
[When browser strip it:]
https://my.site.com/about#hash --> (=Call root > Folder/File name about + Anchor)
https://my.site.com/about/#hash --> (=Call root > Folder name about + Anchor)
https://my.site.com/about.php#hash --> (=Call root > File name about.php + Anchor)
[If not?]
https://my.site.com/folder#name/?query#hash
https://my.site.com/folder.name/about.php?query=one/two