Здравствуйте.
Многим наверняка приходилось в своей жизни проектировать и разрабатывать RESTful API. С релизом технологии Web API делать это стало гораздо проще, а с выходом Web API 2 еще и куда приятней. Система раутинга, перекочевавшая из ASP.NET MVC, отлично справляется со своей задачей, и позволяет нам не только свободно конструировать пути, но и приправлять их различными параметрами, указывая оные в фигурных скобках. Вряд ли шаблон вида «api/{controller}/{id}» вызывает нынче у кого-то благоговейный ужас. Однако что произойдет, если какой-то из методов нашего API в качестве этого самого {id} будет принимать не число в строковом представлении, не Guid, а, скажем, адрес электронной почты? Ну, например, чтобы проверить наличие этого адреса в базе данных. Работать тогда ничего не будет, а виной всему маленькая и, казалось бы, совсем безобидная точка. Как с этим жить дальше и рассказывается под катом.
Сразу стоит оговориться, что само существование проблемы не то, чтобы надуманное, но, скажем так, легко обходимое двумя вполне стандартными путями:
— не надо выделываться, email можно передавать в модели;
— не надо выделываться, email можно передавать как параметр строки запроса.
С другой стороны, когда электронная почта является именно что идентификатором, вроде как логически и идеологически вернее помещать ее именно в путь, а не в модель или атрибут. Впрочем, спорить и дискутировать на эту тему, я уверен, можно долго, перед нами же стоит вполне конкретная проблема: наличие точки в email'е приводит к 404-й ошибке. Первопричина, думаю, вполне прозрачна: веб-сервер пытается искать файл (точка же), а файла такого, разумеется, нет.
Как же с этим можно бороться? Способов много, все они разные, вероятно даже не все возможные появятся ниже (надеюсь, комментарии нам в помощь).
Способ 1.
Слэш. Если мы добавим его в конец пути после нашего праметра {id}, то все внезапно заработает. Все потому, что пониматься этот сегмент будет не как имя файла, а как имя папки. Решение очень простое, но не очень, согласитесь, красивое, ведь придется информировать потребителя сервиса, что именно для данного метода слэш в конце ставить надо, а для других — вовсе необязательно (да их обычно никто и не ставит). Здесь же стоит упомянуть еще один интересный момент, связанный с папками и точками. Если мы поставим точку в конце сегмента прямо перед слэшем, то тоже получим 404. Не могут, дескать, имена папок в пост-MSDOS системах (а ноги растут именно оттуда) оканчиваться точкой. По этой же причине в пути нелья использовать следующие литералы: COM1-9, LPT1-9, AUX, PRT, NUL, CON. Есть, конечно, способ обойти и это, в частности ипользовав атрибут
<httpRuntime relaxedUrlToFileSystemMapping="true" />
Способ 2.
В разделе system.webServer файла конфигурации прописываем
<modules runAllManagedModulesForAllRequests="true" />
и вуаля, все работает. Плохой способ, очень плохой, хотя многие сходу советуют именно его. Когда-то без этой опции действительно было просто не обойтись, но по большому счету она означает, что вместо нативных модулей для получения статического контента, такого как изображения, CSS, скрипты и т.п., каждый запрос будет проходить через весь набор управляемых модулей вашего приложения, что чревато значительными потерями производительности, как минимум. Именно поэтому подобное решение — это своеобразный такой «брутфорс».
Способ 3.
Желаемого эффекта мы можем добиться и следующей настройкой все в том же файле конфигурации:
<modules>
<remove name="UrlRoutingModule-4.0" />
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition="" />
</modules>
Здесь суть заключается в том, что мы очищаем атрибут preCondition у UrlRoutingModule, разрешая таким образом данному управляемому модулю обрабатывать все входящие запросы. Способ очень похож на предыдущий с той лишь разницей, что там каждый запрос обрабатывали все управлямые модули, а здесь только один. Хрен, в общем-то, редьки не слаще.
Способ 4.
Еще в далеком 2010-м году Microsoft выпустила патч для IIS 7.0 и 7.5, который позволил хэндлерам с атрибутом path="*." понимать не только пути, оканчивающиеся точкой, но и пути без оной. Так наступила эра т.н. extensionless URLs, а наш способ номер два потерял актуальность. Описание патча, кстати говоря, можно найти здесь. Нынче же в своем web.config'е запросто можно обнаружить подобную запись в разделе handlers:
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
Как это решает нашу проблему? Да в общем-то никак. Разве что значение атрибута path мы поменяем на сиротливую звездочку. Тоже своего рода выход, и наш email даже заработает, правда ни одного файла с расширением мы больше не достанем, т.к. будем получать справедливую 500-ю ошибку от этого же самого хэндлера.
Возникает вопрос: так что же делать? Ответ на него отднозначным быть, наверное, не может. Лучшим решением на мой сугубо личный взгляд здесь является использование последнего способа, но только частично. Скажем, если мы знаем, что все пути к методам (и только методам, а не статическим ресурсам!) API начинаются с соответствующего префикса («api/{controller}/{id}»), то мы можем добавить еще один хэндлер того же типа именно для адресов вида «api/*»:
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<add name="API-ExtensionlessUrlHandler-Integrated-4.0" path="api/*" verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
Таким образом мы, что называется, отделяем мух от котлет на уровне проектирования API.
P.S. Если у кого есть другие способы решения подобной проблемы, равно как и дополнительные сведения и разъяснения, то в комментариях всячески приветствуется высказывание своих и не только своих умных мыслей. Спасибо за внимание.
Автор: Cromathaar