Learning from bad examples
我当时正在帮助一个需要将物业管理系统中的房屋可用性与客户网站集成的朋友。 幸运的是,物业管理系统具有API。 而不幸的是,这一切都错了。
这个故事的目的不是给二手系统带来不好的广告,而是分享不应该开发的东西,以及在设计API时学习正确的方法。
我朋友的客户正在使用Beds24系统管理他们的财产清单,并在各种预订系统(预订,AirBnB等)之间保持可用性同步。 他们正在建立一个网站,并希望搜索机制仅显示可用于所选日期和客人人数的属性。 听起来像是一项艰巨的任务,因为Beds24提供了与其他系统集成的API。 不幸的是,开发人员在设计时已经犯了很多错误。 让我们逐步解决这些错误,看看到底出了什么问题以及应该如何解决。
由于只对获取客户端属性的可用性感兴趣,因此仅对/ getAvailabilities调用感兴趣。 即使这是获取可用性的调用,但实际上这是一个POST请求,因为作者决定接受过滤器作为JSON正文。 以下是所有可能参数的列表:
{
"checkIn": "20151001",
"lastNight": "20151002",
"checkOut": "20151003",
"roomId": "12345",
"propId": "1234",
"ownerId": "123",
"numAdult": "2",
"numChild": "0",
"offerId": "1",
"voucherCode": "",
"referer": "",
"agent": "",
"ignoreAvail": false,
"propIds": [
1235,
1236
],
"roomIds": [
12347,
12348,
12349
]
}
让我遍历JSON对象并解释其参数出了什么问题。
· 日期checkIn,lastNight和checkOut使用YYYYMMDD格式设置。 将日期编码为字符串时,绝对没有理由不使用标准ISO 8601格式(YYYY-MM-DD),因为这是大多数开发人员和JSON解析器都理解并期望的广泛采用的标准。 除此之外,lastNight字段似乎是多余的,因为提供了checkOut,它总是比前一天晚一天。 请始终使用标准日期编码格式,不要要求API用户提供冗余数据。
· 所有Id以及numAdult和numChild字段都是数字,但被编码为字符串。 在这种特殊情况下,似乎没有理由将它们编码为字符串。
· 我们具有以下字段对:roomId和roomIds以及propId和propIds。 因为可以使用roomIds和propIds传递ID,所以具有roomId和propId属性不仅是多余的,而且这里还存在类型问题。 请注意,roomId需要一个字符串,而roomIds需要一个数字数组。 这可能会造成混乱,解析问题,并且意味着即使我们在谈论相同的数据,后端本身也会对字符串执行某些操作,并对数字执行某些操作。
请不要将开发人员犯此类愚蠢的错误,并尝试使用标准格式,并注意冗余和字段类型。 不要只是将所有内容包装在字符串中。
如上一部分有关请求正文格式的说明,我们仅关注/ getAvailabilities调用。 这次让我们看一下响应主体格式,看看有什么问题。 请记住,我们有兴趣获取给定日期和许多客人可用的属性的ID。 以下是相应的请求和响应正文:
请求:
{
"checkIn": "20190501",
"checkOut": "20190503",
"ownerId": "25748",
"numAdult": "2",
"numChild": "0"
}
响应:
{
"10328": {
"roomId": "10328",
"propId": "4478",
"roomsavail": "0"
},
"13219": {
"roomId": "13219",
"propId": "5729",
"roomsavail": "0"
},
"14900": {
"roomId": "14900",
"propId": "6779",
"roomsavail": 1
},
"checkIn": "20190501",
"lastNight": "20190502",
"checkOut": "20190503",
"ownerId": 25748,
"numAdult": 2
}
· ownerId和numAdult在响应中突然变成数字,而不是在请求正文中为字符串。
· 没有属性列表。相反,属性是使用roomId作为键的顶级对象。这意味着,为了获取可用属性的列表,我们需要遍历所有对象,检查是否存在某些参数(例如roomsavail),舍弃其他参数(例如checkIn,lastNight等)以获取属性列表。然后,我们需要检查roomsavail属性的值,如果该值大于0,则将该属性视为可用。但是请稍等,在这里仔细查看:" roomsavail":" 0",在这里" roomsavail":1.看到模式了吗?如果没有可用的房间,则值为字符串,而如果有可用的房间,则为数字!这将在诸如JAVA之类的强制执行类型安全的语言中引起很多问题,因为同一属性不应属于不同的类型。请使用适当的JSON列表来显示数据集合,而不要像上面那样使用奇怪的键值构造,并确保不要在对象之间更改字段类型。正确格式的响应如下所示(请注意,这种格式也可以在不重复任何内容的情况下获取有关房间的信息):
{
"properties": [
{
"id": 4478,
"rooms": [
{
"id": 12328,
"available": false
}
]
},
{
"id": 5729,
"rooms": [
{
"id": 13219,
"available": false
}
]
},
{
"id": 6779,
"rooms": [
{
"id": 14900,
"available": true
}
]
}
],
"checkIn": "2019-05-01",
"lastNight": "2019-05-02",
"checkOut": "2019-05-03",
"ownerId": 25748,
"numAdult": 2
}
此API中的错误处理是通过以下方式实现的:即使发生错误,所有请求也会返回200的响应代码。 这意味着除了解析正文并检查是否存在error或errorCode字段之外,没有其他方法可以区分响应是成功还是失败。 该API仅包含6个错误代码,如下所示:
Error codes of Beds24 API
请考虑在出现问题时不要使用这种返回响应代码200(成功)的方法,除非这是在API框架中使用的标准方法。 优良作法是使用大多数客户端和开发人员都可以识别的标准HTTP错误代码。 例如,以上屏幕快照中的错误代码1009应该替换为401(未经授权)HTTP代码。 如果API客户端可以预先知道是否解析主体以及如何解析主体(作为数据对象或错误对象),则将使工作变得更轻松。 如果错误是特定于应用程序的,则最好在响应正文中返回400(错误请求)或500(服务器错误)以及相应的错误消息。
对于给定的API选择哪种错误处理策略,只要确保它是一致的并符合广泛采用的HTTP标准即可。 这将使我们的生活更轻松。
以下是文档中API使用的"准则":
使用API时请遵守以下准则
1.呼叫应设计为仅发送和接收所需的最少数据。
2.一次仅允许一个API调用,您必须等待第一个调用完成才能开始下一个API调用。
3.多个呼叫之间的间隔应间隔几秒钟。
4. API调用应尽量少用,并保持在合理的业务使用所需的最低限度内。
5. 5分钟内过度使用将导致您的帐户被封锁,而不会发出警告。
6.我们保留自行决定禁用任何我们认为过度使用API函数的访问的权利,恕不另行通知。
虽然第1点,第4点有意义,但我不同意其他观点。 让我解释一下原因。
2.如果您正在构建REST API,则应假定该API是无状态的,在任何时间点都不应存在任何状态。 这是REST在云应用程序中有用的原因之一。 如果发生故障,可以自由地重新部署无状态组件,并且它们可以根据负载变化进行扩展。 请确保在设计RESTful API时,它实际上是无状态的,并且开发人员无需关心"一次请求"之类的事情。
3.这是一个模棱两可,非常奇怪的准则。 不幸的是,我无法弄清楚作者创建此"指南"的原因,但是它给人一种感觉,即在请求本身之外进行了一些处理,因此,紧接彼此进行调用可能会使系统处于某种错误状态 。 同样,作者说"几秒钟"的事实并没有提供有关两次请求之间实际时间的具体信息。
再次参见图5和6。在这种情况下,没有解释什么是"过度"。 是每秒10个请求还是1个? 此外,某些网站将拥有大量流量,并且仅由于这些原因而阻止它们使用API,而不会发出任何警告,可能会使开发人员远离此类系统。 请在制定此类指南时具体说明,并在提出此类规则时考虑用户。
API文档的外观如下所示:
Beds24 API documentation look&feel
这里唯一的问题是可读性和整体感觉。 如果作者使用markdown而不是自定义非样式html,则同一文档的外观可能会更好。 为了这篇文章的缘故,我在Dilliger的帮助下不到2分钟创建了一个更好的版本。 结果如下:
Styled version of the documentation
请使用工具创建API文档。 对于简单的文档,上面的一个markdown文件就足够了,而对于更大,功能更丰富的文档,最好使用Swagger或Apiary之类的工具。
这是Beds24 API文档的链接,适合那些想要自己看一下的人。
API文档规定了关于所有端点的以下内容:
要使用这些功能,必须在菜单设置>>帐户>>帐户访问中允许API访问。
但是,实际上,任何人都可以请求获取数据,而无需为某些调用传递任何凭据,例如获取特定属性的可用性。 在文档的不同部分中对此进行了说明:
大多数JSON方法都需要API密钥才能访问帐户。 可以在菜单>>帐户>>帐户访问中设置API代码。
除了上面关于身份验证的沟通不畅之外,API密钥实际上是用户需要自己创建的东西(实际上是手动键入密钥,绝不自动生成),并且其长度应在16到64个字符之间。 允许用户自己创建密钥可能会导致非常不安全的密钥(很容易猜到)或某些格式问题,因为可以在帐户设置的该字段中输入任何字符串。 在最坏的情况下,它也可能成为SQL注入或其他类型攻击的门户。 请不要让用户创建API密钥。 始终为他们提供不可变的自动生成的密钥,并能够在需要时使其无效。
对于那些实际经过身份验证的请求,我们有一个不同的问题:身份验证令牌必须作为请求主体的一部分发送,如下所示(从文档中可以看出):
Beds24 API authentication example
在请求正文中包含身份验证令牌意味着服务器将需要首先解析请求正文,提取密钥,执行身份验证,然后决定如何处理请求:是否执行请求。 如果成功通过身份验证,则不会涉及任何开销,因为无论如何都将解析该正文。 万一验证失败,服务器将完成上述所有工作,只是提取令牌,从而浪费宝贵的处理时间。 相反,更好的方法是使用Bearer身份验证方案或类似方法将身份验证令牌作为请求标头发送。 这样,服务器仅在成功认证的情况下才需要解析请求主体。 使用诸如Bearer令牌之类的标准身份验证方案的另一个原因仅仅是因为大多数开发人员都熟悉它。
最后但并非最不重要的一点是,请求完成平均需要1秒钟多一点的时间。 在现代应用中,这种延迟可能是不可接受的。 因此,在设计API时要考虑性能。
尽管上面解释了API的所有问题,但它确实可以完成。 但是,对于开发人员来说,理解和实现它需要花费数倍的时间,并且需要花费大量时间来编写更复杂的解决方案以解决琐碎的问题。 因此,在发布API之前,请考虑让开发人员实现您的API。 确保文档完整,清晰且格式正确。 检查您的资源名称是否遵循约定,数据结构是否正确,易于理解和使用。 另外,请注意安全性和性能,不要忘记正确执行错误处理。 如果在设计API时将上述所有因素都考虑在内,那么就不需要像前面的示例中那样奇怪的"准则"。
如前所述,这篇文章的目的不是让您永远不要使用Beds24或任何类似的系统,因为它们的API没有正确实现。 目标是通过分享一个不好的例子并解释如何更好地完成软件来提高软件产品的质量。 希望这篇文章可以使某人更多地关注软件开发最佳实践,并使软件系统更好。 直到下一次!
(本文翻译自Robert Konarskis的文章《How NOT to design APIs》,参考:https://blog.usejournal.com/how-not-to-design-restful-apis-fb4892d9057a)