了解REST:动词,错误代码和authentication
我正在寻找一种方法来在我的基于PHP的Web应用程序,数据库和CMS中将API包装在默认函数中。
我环顾四周,发现了几个“骨架”框架。 除了我的问题的答案,还有Tonic ,我喜欢的REST框架,因为它非常轻便。
我喜欢REST,因为它的简单性,并且希望创build一个基于它的API体系结构。 我试图让我的头脑基本原则,还没有完全理解。 因此,一些问题。
我的理解是正确的吗?
说我有一个资源“用户”。 我可以设置一些URI,如下所示:
/api/users when called with GET, lists users /api/users when called with POST, creates user record /api/users/1 when called with GET, shows user record when called with PUT, updates user record when called with DELETE, deletes user record
这是迄今为止RESTful架构的正确表示吗?
我需要更多的动词
创build,更新和删除在理论上可能是足够的,但在实践中,我将需要更多的动词。 我意识到这些是可以embedded到更新请求中的东西,但它们是可以具有特定返回代码的特定操作,我不想将它们全部置于一个操作中。
在用户例子中想到的一些是:
activate_login deactivate_login change_password add_credit
我将如何expression诸如在RESTful URL架构中的操作?
我的直觉是做一个GET调用一个URL
/api/users/1/activate_login
并期待一个状态码回来。
这偏离了使用HTTP动词的想法。 你怎么看?
3.如何返回错误消息和代码
REST美丽的一大部分来源于它使用标准的HTTP方法。 发生错误时,我会发出带有3xx,4xx或5xx错误状态码的标题。 对于详细的错误描述,我可以使用正文(对吧?)。 到现在为止还挺好。 但是传输专有错误代码的方式是什么?在描述什么地方出错(比如“连接数据库失败”或者“数据库login错误”)时会更加详细。 如果我把这些信息和信息一起放进身体里,我必须事后parsing出来。 这种东西是否有标准的标题?
4.如何做authentication
- 基于REST原则的基于API密钥的authentication是什么样的?
- 在对REST客户端进行身份validation时使用会话有什么好处,除了这是公然违反REST原则? :)(这里只有一半是开玩笑,基于会话的身份validation可以和我现有的基础架构配合。)
我注意到这个问题晚了几天,但我觉得我可以增加一些见解。 我希望这可以有助于您的RESTful风险投资。
要点1:我理解对吗?
你理解正确。 这是一个RESTful体系结构的正确表示。 你可能会发现维基百科下面的matrix在定义你的名词和动词方面非常有帮助:
处理Collection URI时,请执行以下操作: http://example.com/resources/
: http://example.com/resources/
-
GET :列出集合的成员,完成其成员URI进一步导航。 例如,列出所有待售汽车。
-
PUT :含义定义为“用另一个集合replace整个集合”。
-
POST :在由集合自动分配ID的集合中创build一个新条目。 创build的ID通常包含在此操作返回的数据中。
-
DELETE :含义定义为“删除整个集合”。
处理会员 URI时,请执行以下操作: http://example.com/resources/7HOU57Y
: http://example.com/resources/7HOU57Y
-
GET : 获取以适当的MIMEtypes表示的收件人地址成员的表示。
-
PUT :更新集合的寻址成员或使用指定的ID创build它。
-
POST :将被提名的成员作为自己的权利集合,并创build一个新的从属。
-
删除 :删除收集的编址成员。
第二点:我需要更多的动词
一般来说,当你认为你需要更多的动词时,这可能意味着你的资源需要被重新识别。 请记住,在REST中,您始终处理资源或资源集合。 你select什么资源对于你的API定义是非常重要的。
激活/取消激活login :如果您正在创build一个新的会话,那么您可能需要将“会话”视为资源。 要创build新会话,请使用POST将http://example.com/sessions/
与身份凭证一起使用。 要过期,请使用PUT或DELETE(可能取决于您是否打算保留会话历史logging)到http://example.com/sessions/SESSION_ID
。
更改密码:这次资源是“用户”。 您需要在http://example.com/users/USER_ID
使用PUT,并在主体中使用旧密码和新密码。 您正在处理“用户”资源,而更改密码只是更新请求。 它与关系数据库中的UPDATE语句非常相似。
我的直觉是做一个像
/api/users/1/activate_login
这样的URL的GET调用
这违背了一个非常核心的REST原则:正确使用HTTP动词。 任何GET请求都不应该留下任何副作用。
例如,GET请求不应该在数据库上创build会话,返回带有新的会话ID的cookie,或者在服务器上留下任何残留。 GET谓词就像数据库引擎中的SELECT语句。 请记住,使用GET动词的任何请求的响应应该是可caching的,当请求具有相同的参数时,就像请求静态网页一样。
第3点:如何返回错误消息和代码
考虑4xx或5xx HTTP状态码作为错误类别。 你可以在正文中详细说明错误。
无法连接到数据库: / 不正确的数据库login :一般来说,您应该为这些types的错误使用500错误。 这是服务器端错误。 客户没有错。 500错误通常被认为是“可重试的”。 即客户端可以重试相同的确切请求,并希望一旦服务器的问题得到解决,它就成功了。 在正文中指定详细信息,以便客户端能够为我们的人类提供一些上下文。
另一类错误是4xx系列,这通常表明客户做错了什么。 特别是,这类错误通常会向客户端指出,不需要重试请求,因为它将继续永久失败。 即客户端在重试这个请求之前需要改变一些东西。 例如,“未find资源”(HTTP 404)或“格式错误的请求”(HTTP 400)错误将属于此类别。
第4点:如何进行身份validation
正如第1点所指出的那样,您可能需要考虑创build会话,而不是对用户进行身份validation。 您将返回一个新的“会话ID”,以及适当的HTTP状态代码(200:访问授权或403:访问被拒绝)。
然后你会问你的RESTful服务器:“你能为我获得这个会话ID的资源吗?”。
没有authentication模式 – REST是无状态的:您创build一个会话,请求服务器为您提供使用此会话ID作为参数的资源,并在注销时删除或过期会话。
简而言之,你是完全落后的。
你不应该使用你应该使用的URL来处理这个问题。 一旦你决定了你的系统需要哪些资源,以及你将如何表示这些资源以及资源和应用程序状态之间的交互,这些URL将会“免费”。
引用Roy Fielding的话
REST API应该花费几乎所有的描述性努力来定义用于表示资源和驱动应用程序状态的媒体types,或者为现有标准媒体types定义扩展关系名称和/或启用超文本的标记。 用于描述在感兴趣的URI上使用什么方法的任何努力应该在媒体types的处理规则的范围内(并且在大多数情况下已经由现有媒体types定义)完全定义。 [这里的失败意味着带外信息正在推动互动,而不是超文本]。
人们总是从URI开始,认为这是解决scheme,然后他们倾向于错过REST体系结构中的一个关键概念,特别是如上所述,“这里的失败意味着带外信息正在推动交互而不是超文本。 “
说实话,许多人看到一堆的URI和一些GET和PUT和POST,并认为REST很容易。 REST并不容易。 RPC over HTTP很简单,通过HTTP有效载荷来回移动数据块很容易。 然而,REST超越了这个范围。 REST是协议不可知的。 HTTP对于REST系统来说非常stream行。
REST存在于媒体types,它们的定义以及应用程序如何通过超文本(有效链接)驱动可用于这些资源的操作。
关于REST系统中的媒体types有不同的看法。 有些支持特定于应用程序的有效负载,而另一些则喜欢将现有媒体types提升为适合应用程序的angular色。 例如,一方面,您可以通过微格式和其他机制,为您的应用程序devise适合您的应用程序的特定XML模式,而不是像使用XHTML这样的代表。
两种方法都有它们自己的位置,我认为,XHTML在人机驱动和机器驱动的networking重叠的场景中工作得非常好,而前者,更具体的数据types,我觉得更好的方便了机器到机器的交互。 我发现商品格式的提升可能会使内容谈判变得困难。 “application / xml + yourresource”作为媒体types比“application / xhtml + xml”更具体,因为后者可以应用于许多有效载荷,这可能是机器客户端实际上感兴趣的东西,也可能不是决定不反省。
但是,XHTML在Web浏览器和渲染非常重要的人类Web中工作得非常好(显然)。
你的应用程序将指导你做这样的决定。
deviseREST系统的一部分工作就是发现系统中的第一类资源,以及支持主资源操作所需的衍生支持资源。 一旦资源被发现,那么这些资源的表示,以及状态图显示的资源stream通过超文本的表示,因为下一个挑战。
回想一下,在超文本系统中,资源的每个表示都将实际的资源表示以及可用于资源的状态转换组合在一起。 考虑每个资源是图中的一个节点,链接是将该节点留给其他状态的行。 这些链接不仅告诉客户可以做什么,而且还要做什么(作为一个好的链接结合了URI和所需的媒体types)。
例如,您可能有:
<link href="http://example.com/users" rel="users" type="application/xml+usercollection"/> <link href="http://example.com/users?search" rel="search" type="application/xml+usersearchcriteria"/>
您的文档将讨论名为“users”的rel字段以及“application / xml + youruser”的媒体types。
这些链接可能看起来多余,他们都在用同一个URI进行交stream。 但他们不是。
这是因为对于“用户”关系,该链接正在讨论用户的集合,并且您可以使用统一接口来处理集合(GET可以检索所有的集合,删除所有的集合等等)
如果您发布到此URL,您将需要传递一个“application / xml + usercollection”文档,该文档可能只包含文档中的单个用户实例,因此您可以添加该用户,或者不添加一旦。 也许你的文档会build议你只需传递一个用户types,而不是集合。
您可以查看应用程序需要执行的search,如“search”链接所定义的,并且是中介types。 search媒体types的文档将告诉你这是如何performance的,以及期待的结果。
然而,这里的外卖是URI本身基本上不重要。 应用程序是在控制的URI,而不是客户端。 除了一些“切入点”之外,客户应该依靠应用程序提供的URI来工作。
客户端需要知道如何操作和解释媒体types,但不需要太在意。
客户眼中这两个链接在语义上是相同的:
<link href="http://example.com/users?search" rel="search" type="application/xml+usersearchcriteria"/> <link href="http://example.com/AW163FH87SGV" rel="search" type="application/xml+usersearchcriteria"/>
所以,专注于你的资源。 关注他们在应用程序中的状态转换以及如何最好地实现。
重新1 :这看起来很好。 请记住,在“Location:”标题中返回新创build的用户的URI,作为对POST的响应的一部分,以及“201 Created”状态码。
重新2 :通过GET激活是一个坏主意,在URI中包含动词是一种devise气味。 您可能要考虑在GET上返回一个表单。 在一个Web应用程序中,这将是一个带有提交button的HTML表单; 在API用例中,您可能想要返回一个包含要PUT的URI的表示来激活该帐户。 当然,你也可以在/ POST用户的响应中包含这个URI。 使用PUT将确保您的请求是幂等的,也就是说,如果客户端不确定成功,它可以再次安全地发送。 一般来说,想一下你可以把什么资源转化成动词(“动词的名词化”)。 问问自己,你的具体行动最接近的是什么方法。 例如change_password – > PUT; 停用 – >可能是DELETE; add_credit – >可能是POST或者PUT。 将客户端指向适当的URI,方法是将其包含在您的表示中。
3.不要发明新的状态代码,除非你认为它们是通用的,它们值得在全球范围内标准化。 尽量使用最合适的状态码(在RFC 2616中阅读所有这些码)。 在响应主体中包含更多信息。 如果你确实想要发明一个新的状态码,那么再想一想; 如果你仍然相信,至less要select正确的类别(1xx – > OK,2xx – > informational,3xx – >redirect; 4xx->客户端错误,5xx – >服务器错误)。 我有没有提到发明新的状态代码是一个坏主意?
4.如果可能的话,使用HTTP内置的authentication框架。 看看Google在GData中进行身份validation的方式。 一般来说,不要在您的URI中放置API密钥。 尽量避免使用会话来增强可伸缩性并支持caching – 如果由于之前发生的事情而导致对请求的响应不同,则通常会将自己绑定到特定的服务器stream程实例。 将会话状态变成任何一种客户端状态(例如,使其成为后续请求的一部分)或通过将其变成(服务器)资源状态来明确,即给它自己的URI是更好的。
你有正确的想法如何devise你的资源,恕我直言。 我不会改变一件事情。
2.与其试图用更多的动词来扩展HTTP,不如考虑基本的HTTP方法和资源,你所提出的动词可以减less到什么程度。 例如,您可以设置资源,如: /api/users/1/login/active
这是一个简单的布尔值),而不是activate_login
动词。 要激活一个login,只要把一个文件那里说“真”或1或其他。 要取消激活, PUT
文件PUT
在空白处或表示为0或false。
同样,要更改或设置密码,只需将PUT
s设置为/api/users/1/password
。
无论何时你需要添加一些东西(比如信用),可以用POST
的方式来思考。 例如,您可以使用包含要添加的点数的主体对/api/users/1/credits
等资源执行POST
。 可以使用相同资源上的PUT
来覆盖该值而不是添加。 带有负数的POST
会减去,等等。
3.我强烈build议不要扩展基本的HTTP状态码。 如果找不到与您的情况完全匹配的人,请select最接近的人,并将错误详细信息放入响应正文中。 另外,请记住HTTP标头是可扩展的。 你的应用程序可以定义你喜欢的所有自定义标题。 例如,我工作的一个应用程序可能会在多种情况下返回404 Not Found
。 我们只是添加了一个新的头部X-Status-Extended
,它包含了我们专有的状态码X-Status-Extended
,而不是让客户端parsing响应主体。 所以你可能会看到如下的回应:
HTTP/1.1 404 Not Found X-Status-Extended: 404.3 More Specific Error Here
这样一个像Web浏览器这样的HTTP客户端仍然可以知道如何处理常规的404代码,而一个更复杂的HTTP客户端可以select查看X-Status-Extended
头文件以获得更具体的信息。
4.对于身份validation,如果可以,我build议使用HTTP身份validation。 但恕我直言,没有错误的基于cookie的身份validation,如果这对你更容易。
对于你说的例子,我会使用以下内容:
activate_login
POST /users/1/activation
deactivate_login
DELETE /users/1/activation
更改密码
PUT /passwords
(这假设用户是authentication的)
add_credit
POST /credits
(这假定用户通过authentication)
对于错误,您将以请求中的格式返回正文中的错误,所以如果您收到:
DELETE /users/1.xml
您将以XML格式发送回应,对于JSON等也是如此。
对于身份validation,您应该使用httpvalidation。
REST基础
REST具有统一的接口约束,该约束声明REST客户端必须依赖标准而不是实际REST服务的特定于应用程序的细节,所以REST客户端不会因微小的更改而中断,并且可能是可重用的。
所以在REST客户端和REST服务之间有一个合同。 如果您使用HTTP作为基础协议,则以下标准是合同的一部分:
- HTTP 1.1
- 方法定义
- 状态码定义
- caching控制标题
- 接受和内容types的标题
- validation标题
- IRI (utf8 URI )
- 身体(挑一个)
- 注册应用程序特定的MIMEtypes,例如迷宫+ xml
- 供应商特定的MIMEtypes,例如vnd.github + json
- 通用MIMEtypes
- 特定于应用的RDF词汇,例如ld + json & hydra , schema.org
- 应用程序特定的configuration文件,例如hal + json和configuration文件链接参数(我猜)
- 超链接
- 什么应该包含他们(挑一个)
- 发送链接标题
- 发送超媒体响应,例如html,atom + xml,hal + json,ld + json&hydra等等。
- 语义
- 使用IANA链接关系和可能的自定义链接关系
- 使用特定于应用程序的RDF词汇表
- 什么应该包含他们(挑一个)
REST具有无状态约束,它声明REST服务和客户端之间的通信必须是无状态的。 这意味着REST服务无法维护客户端状态,因此您不能拥有服务器端会话存储。 你必须authentication每一个请求。 因此,例如HTTP基本身份validation(HTTP标准的一部分)是可以的,因为它发送每个请求的用户名和密码。
回答你的问题
-
是的,可以。
只要提到,客户不关心IRI结构,他们关心语义,因为他们遵循具有链接关系或链接数据(RDF)属性的链接。
对于IRI唯一重要的是,一个IRI必须只识别一个资源。 允许单个资源(如用户)拥有许多不同的IRI。
为什么我们使用像
/users/123/password
这样的好IRI是非常简单的; 只要读懂IRI,理解服务器上的路由逻辑就容易多了。 -
你有更多的动词,比如PUT,PATCH,OPTIONS,甚至更多,但是你不需要更多的动词…而不是添加新的动词,你必须学习如何添加新的资源。
activate_login -> PUT /login/active true deactivate_login -> PUT /login/active false change_password -> PUT /user/xy/password "newpass" add_credit -> POST /credit/raise {details: {}}
(由于无状态约束,从REST的angular度来看login没有意义。)
-
您的用户不关心为什么存在这个问题。 他们只想知道是否有成功或错误,并可能是一个他们可以理解的错误信息,例如:“对不起,但我们无法保存您的post”等。
HTTP状态标题是您的标准标题。 其他一切都应该在我认为的身体里。 单个头文件不足以描述详细的多语言错误消息。
-
无状态约束(以及caching和分层系统约束)确保了服务的良好扩展。 你当然不想在服务器上维护数百万个会话,当你可以在客户端上做同样的事情…
如果用户使用主客户端授予访问权限,则第三方客户端将获得访问令牌。 之后,第三方客户端发送每个请求的访问令牌。 还有更复杂的解决scheme,例如,您可以签署每一个请求等。有关更多详细信息,请查看OAuth手册。
相关文献
- build筑风格与基于networking的软件体系结构的devise
Roy Thomas Fielding的论文(REST的作者)
2000年,加州大学欧文分校 - 第三代Web API – 缩小REST和关联数据之间的差距
Markus Lanthaler的论文(JSON-LD的合着者和Hydra的作者)
2014年,格拉茨技术大学,奥地利
- 当你不知道新的资源URI是怎么样的时候使用post(你创build新的用户,应用程序会分配新用户它是id),PUT用于更新或创build你知道它们将如何表示的资源(例如:PUT /myfiles/thisismynewfile.txt)
- 返回消息体中的错误描述
- 您可以使用HTTP身份validation(如果足够),Web服务应该是状态
我会build议(作为第一通) PUT
应该只用于更新现有的实体。 POST
应该用于创build新的。 即
/api/users when called with PUT, creates user record
我感觉不舒服 但是,第一部分的其余部分(重复使用动词)看起来是合乎逻辑的。
详细的,但从HTTP 1.1方法规范复制http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
9.3 GET
GET方法意味着检索任何信息(以实体的forms)由Request-URI标识。 如果Request-URI指的是一个数据产生过程,那么产生的数据应该作为响应中的实体而不是过程的源文本被返回,除非该文本恰好是过程的输出。
如果请求消息包含If-Modified-Since,If-Unmodified-Since,If-Match,If-None-Match或If-Range标题字段,则GET方法的语义变为“条件GET”。 一个有条件的GET方法请求仅在条件头字段描述的情况下传送实体。 条件GET方法旨在通过允许caching实体刷新而不需要多个请求或传送客户端已经拥有的数据来减less不必要的networking使用。
如果请求消息包含Range标头字段,则GET方法的语义变为“部分GET”。 部分GET请求只有部分实体被转移,如14.35节所述。 部分GET方法旨在通过允许完成部分检索的实体而不传送已经由客户端持有的数据来减less不必要的networking使用。
对GET请求的响应是可caching的,当且仅当它符合第13节中描述的HTTPcaching的要求。
用于表单的安全性考虑见15.1.3节。
9.5 POST
POST方法用于请求源服务器接受请求中包含的实体作为Request-Line中Request-URI标识的资源的新下属。 POST被devise为允许统一的方法来覆盖以下function:
- Annotation of existing resources; - Posting a message to a bulletin board, newsgroup, mailing list, or similar group of articles; - Providing a block of data, such as the result of submitting a form, to a data-handling process; - Extending a database through an append operation.
POST方法执行的实际function由服务器决定,通常取决于Request-URI。 所发布的实体从属于该URI,其方式与文件从属于包含它的目录相同,新闻文章从属于发布的新闻组,或者logging从属于数据库。
POST方法执行的操作可能不会导致可以通过URI标识的资源。 在这种情况下,根据响应是否包含描述结果的实体,200(OK)或204(无内容)是适当的响应状态。
如果源服务器上已经创build了一个资源,那么响应应该是201(创build),并且包含一个描述请求状态的实体,并引用新资源和一个位置标题(见14.30节)。
除非响应包含适当的Cache-Control或Expires头字段,否则对此方法的响应不可caching。 但是,303(请参阅其他)响应可用于指导用户代理检索可caching资源。
POST请求必须遵守8.2节中规定的消息传输要求。
出于安全考虑,请参阅第15.1.3节。
9.6 PUT
PUT方法要求封闭的实体存储在提供的Request-URI下。 如果Request-URI引用一个已经存在的资源,封闭的实体应该被认为是驻留在原始服务器上的修改版本。 如果Request-URI不指向现有资源,并且该URI能够被请求用户代理定义为新资源,则源服务器可以使用该URI创build资源。 如果新资源被创build,源服务器必须通过201(创build)响应通知用户代理。 如果现有资源被修改,则应该发送200(OK)或204(无内容)响应代码以指示成功完成请求。 如果无法使用Request-URI创build或修改资源,则应给出适当的错误响应,以反映问题的性质。 实体的接收者不能忽略任何不理解或实现的Content- *(例如Content-Range)头,在这种情况下必须返回一个501(未实现)响应。
如果请求通过caching并且Request-URI标识了一个或多个当前caching的实体,那么这些条目应该被视为陈旧。 对此方法的响应不可caching。
POST和PUT请求之间的根本区别反映在Request-URI的不同含义中。 POST请求中的URI标识将处理封闭实体的资源。 该资源可能是数据接受过程,某个其他协议的入口,也可能是接受注释的单独实体。 相比之下,PUT请求中的URI标识了请求中包含的实体 – 用户代理知道哪个URI是预期的,服务器不能尝试将请求应用到其他资源。 如果服务器希望将请求应用于不同的URI,
它必须发送301(永久移动)响应; 用户代理可以自行决定是否redirect请求。
单个资源可能被许多不同的URI识别。 例如,一篇文章可能有一个用于标识“当前版本”的URI,它与识别每个特定版本的URI是分开的。 在这种情况下,普通URI上的PUT请求可能会导致其他几个URI被源服务器定义。
HTTP / 1.1没有定义PUT方法如何影响源服务器的状态。
PUT请求必须遵守8.2节中规定的消息传输要求。
除非特定的实体头文件另外指定,否则PUT请求中的实体头文件应该被应用到由PUT创build或修改的资源。
9.7删除
DELETE方法请求源服务器删除由Request-URI标识的资源。 这种方法可能会被原始服务器上的人为干预(或其他方式)覆盖。 即使从原始服务器返回的状态码指示操作已成功完成,客户端也不能保证已执行操作。 但是,服务器不应该表示成功,除非在给出响应时,它打算删除资源或将其移动到不可访问的位置。
如果响应包括描述状态的实体,则成功的响应应该是200(OK),如果该操作尚未实施,则成功响应202(接受),如果该操作已经被实施,则响应不包括204(无内容)一个实体。
如果请求通过caching并且Request-URI标识了一个或多个当前caching的实体,那么这些条目应该被视为陈旧。 对此方法的响应不可caching。
关于REST返回代码:混合HTTP协议代码和REST结果是错误的 。
但是,我看到很多的实现混合在一起,很多开发者可能不同意我的看法。
HTTP返回码与HTTP Request
本身相关。 REST调用是使用超文本传输协议请求完成的,它的工作级别低于调用的REST方法本身。 REST is a concept/approach, and its output is a business/logical result, while HTTP result code is a transport one.
For example, returning "404 Not found" when you call /users/ is confuse, because it may mean:
- URI is wrong (HTTP)
- No users are found (REST)
"403 Forbidden/Access Denied" may mean:
- Special permission needed. Browsers can handle it by asking the user/password. (HTTP)
- Wrong access permissions configured on the server. (HTTP)
- You need to be authenticated (REST)
And the list may continue with '500 Server error" (an Apache/Nginx HTTP thrown error or a business constraint error in REST) or other HTTP errors etc…
From the code, it's hard to understand what was the failure reason, a HTTP (transport) failure or a REST (logical) failure.
If the HTTP request physically was performed successfully it should always return 200 code, regardless is the record(s) found or not. Because URI resource is found and was handled by the http server. Yes, it may return an empty set. Is it possible to receive an empty web-page with 200 as http result, right?
Instead of this you may return 200 HTTP code and simply a JSON with an empty array/object, or to use a bool result/success flag to inform about the performed operation status.
Also, some internet providers may intercept your requests and return you a 404 http code. This does not means that your data are not found, but it's something wrong at transport level.
From Wiki :
In July 2004, the UK telecom provider BT Group deployed the Cleanfeed content blocking system, which returns a 404 error to any request for content identified as potentially illegal by the Internet Watch Foundation. Other ISPs return a HTTP 403 "forbidden" error in the same circumstances. The practice of employing fake 404 errors as a means to conceal censorship has also been reported in Thailand and Tunisia. In Tunisia, where censorship was severe before the 2011 revolution, people became aware of the nature of the fake 404 errors and created an imaginary character named "Ammar 404" who represents "the invisible censor".