CGI中的错误处理

 

假设最坏情况或者假设最好情况
错误处理中进行乐观的假设还是进行悲观的假设能很大程度地影响代码的质量。

创建有效的错误信息
错误信息的内容对自己和W6b站点的访问者都是很重要的。如果没有精确的技术信息就不能解决问题。如果没有清楚的、简单的指示,访问者也就不知道该干什么了。

检查常见错误
幸运的是,CGI脚本的先行者们已经预见到了大部分可能的错误,本节将介绍一些常见的错误以及如何避免它们。
到目前为止,已经对CGI脚本进行了调试和测试工作了,用户可能会发现一些自己希望返回的错误而不是输出给用户,然而也有可能是因为客户输入的信息不正确,或是系统中某个资源不可用,或是脚本有一个错误而想发送这些错误信息,任何程序都会发生不计其数的错误。如何检测出这些错误并返回它们的信息几乎与是什么导致错误一样重要,理智周密地进行错误处理是CGI脚本和Web站点的一个重要的最后的问题。

错误处理是任何应用程序的重要部分,但是因为Web 上的非熟练的用户可能多于任何其他的在线领域,所以不仅要考虑将什么作为错误返回给用户,而且要考虑如何去做。不一致的,太一般的或者太技术化的错误信息不仅会使用户糊涂而且会使他们疏远Web。

应象考虑程序中其他问题一样来考虑错误处理和错误消息。理想的系统是一个易于使用、易于理解并且很完整——总而言之,即是用户们所期望的系统。

1.错误处理的两个方面

错误处理可以分为两部分——发现错误和显示错误信息。尽管每部分都很重要,但两者的目的不同。发现错误对自己、编程人员来说很重要,而错误信息的显示对用户而言更重要。


注意
作为一个编程人员,可能会发现自己的目标和用户的目标不尽相同。两者都要仔细考虑,不过任何情况下都要记住编程是为了用户。

处理错误的第一部分是检测它,尽管用户永远不知道CGI程序是如何捕获一个错误的,但很好地检测出错误对服务器的完整性和安全性而言非常重要(甚至自己的工作和名誉)。如果有错误未检测到,它不仅将未被报告也未解决,而且它还很容易导致其他问题——甚至破坏安全性。
从用户角度来说更重要是错误处理的第二部分——如何报告错误,如何解释错误,以及如何更正错误。尽管有些编程人员可能会很注意脚本是不是有编号为42的错误,但普通用户可能不一定这么认为。他们甚至都不知道这是什么意思。不过用户关心的是有没有提供关于如何纠正错误的指导信息。如果面对一个没有任何指导信息的错误信息,他们可能会离开这个站点。

2.错误检测

正如前文所述,处理错误的第一部分是准确地检测它。尽管这一点似乎很显然,但可以有三种方式来完成这个任务。选择哪种方式不仅影响到如何完成错误的处理,还能影响到自己的CGI脚本是不是友好,是不是易于使用。

在代码中如何检测错误能很好地反映一个人的生活态度:那些大胆的愚蠢的人简单地忽略了错误检查,认为不可能出错的。乐观主义者会检查错误但是认为出错的实际可能性很小,因而不去花太多精力去解释。·悲观主义者一尽管这些人在聚会上没多大意思一一但他们经常能写出最好的代码,他们不仅检测错误而且还单独标识每个错误以便能很容易跟踪。在编写CGI脚本时,预期越差,写出来的代码就会越好。

2.1 不实际的假设

当然,在CGI 脚本中最简单的错误处理方式是什么也不处理。只要不检测错误,也就不需要处理它们了。这种方式看起来不错,但却可能非常危险——因为实际上总有地方出错,而程序将对此无能为力。

程序清单1 假设不出错误的代码


#!/usr/bin/perl

#Set up the file to dump
$dump_file = "/etc/motd";
print("content-type:text/html\n\n");

# Open, read and dumpthe fIle
Open(DUMP_FILE,$dump_File);
read(DUMP_FILE,$dum_Text,4096);
print (" \n");
print ("

\n");
print ("$dump_Text\n");
print ("

\n"); 该CGI程序一般情况下运行得很好。但是,如果万一/etc/motd文件不能被打开或无法读取,脚本将或者产生一个错误或者流产,或者更坏的情况——它简单地继续执行,得到不正确的或不完全的结果。 实际上花费一些精力进行错误检测是编写CGI代码的重要部分,在Web上假定一切都不会出错与在别的计算方面这样假设一样愚蠢。 2.2 乐观的假设 在决定检测错误之后,仍有两种方式来编写检测代码,选择哪种方式反映了编程人员对某个子程序会不会出错的信心如何,因为乐观地认为一切都正常而不检测错误是一个错误,而过分相信自己的检测也不对。程序清单2 是一个基干乐观假设的小的PerI CGI脚本。这样如果每个语句都按预期进行工作,流程就继续进行。从这个角度来说,它不是成功的代码。 程序清单2 基于最好假设的代码 #!/usr/bin/perl #Set up the file to dump $dump_File = "/etc/motd"; print("content-type:text/html\n\n"); #Try to open the dump file if (open(DUMP_FILE,$dump_File) == 0) { # Try t0 read the dump file if (read(DUMP_FILE,$dump_Text,4096) > 0) { # Send the dump file print ("\n"); prlnt ("\n"); print ("

\n");
print ("$dump_text\n");
print ("

\n"); exit(0); } } #if we reached here,something went wrong print("\n"); print("The Message of the Day could not be readl\n"); prlnt("\n"); exit (-1); 程序清单2 的错误在于它假定每个调用都会成功。尽管对Open()的和read()调用进行了检测,并且如果它们失败有一条不同的代码路径,但该程序隐含的基本观点是假设调用都能按计划完成。尽管这些调用大多数情况下都很正常,但是万一它们失败,代码中几乎没有任何地方告诉用户出错的原因。 因为程序流程继续预期每个调用都正常执行——因为代码做了乐观的假设——错误报告就像事后考虑一样,像钉到一件极不可能的事件程序并不完全按计划运行上的某个东西。按这种方式编写CGI程序如果失败是很难理解的,因为该脚本可能产生的所有错误都合成了一条简单的、通用的信息。不过还有一种较好的方式。 2.3 悲观的假设 尽管这不是一种广为接受的生活态度,但在编写CGI 脚本时假定什么都会出错却很有好处。如果不是假设每个调用都正常工作,而假定要会失败,那么就可能会写出详细的,有针对性的错误消息来解释是哪个调用出错以及它出错的原因。程序清单3即是程序清单2按这种方式进行重写的结果。 程序清单3 最坏假设的代码 #!/usr/bin/perl #Set up the flle to dump $dump_File = "/etc/motd"; #print a fatal error and exit sub error_Fatal { print("\n"); print("

Error!

Please report the following to the"); print("Webmaster of this site:

\n"); print("@_\n"); print("

/n); exit(-1); } #Print the header print("content-type:text/html\n\n"); #Try to open the dump tile if (open(DUMP_FILE,$dump_File) !=0 ) { print("\n"); print("

Error!

\n"); print("


Could not open MOTD file!>


\n"); print("/BODY>\n"); exit(-1); } #Try to read the dump file if (read(DUMP_FILE,$dump_Text,4096) < 1) { print("\n"); print("

Error!

\n"); print("


Could not read MOTD file!


\n"); print("\n"); exit(-1); } #Send the dumpfile print(""); print(""); print("

\n");
print("$dump_Text\n");
print("

\n"); exit(0); 请注意程序清单3 中的一些地方。最主要的一点是该清单假定每个调用都会失败;由此,它就一定比程序清单2 提供了更详细的有关哪个调用出错的信息。这种信息对于确定出错原因非常有用,并且是改进错误检测代码的一种最好的方式。 提示 在CGI脚本中检测错误时,一定要检查预期的错误的所有可能情况,例如,程序清单3检查了read()是否返回值少于一个。尽管0 是有效的返回值(文件可能为空),也不希望脚本继续执行似乎调用成功一样而在屏幕上什么也没打印。这样,-1(发生了错误)和0(没有读取到内容)都作为错误进行了处理。 另外还要注意,因为只有负的比较才会继续代码的主要流程,程序清单3 显示的程序更好地沿左边界对齐,如果在可以给用户一个应答之前必须嵌套15或20个比较,那可能代码就不得不成锯齿状,甚至越出屏幕右边界。尽管对齐是一件小事,但却能使大程序好读得多。 注意: 许多人都说没有一点错误检测用代码最容易读,而包含大量If语句的错误检测代码很容易让人糊涂。这是事实。在加入错误检测代码后,通常一个小而简单的过程会变得很大。 不过这种代码导致了额外的输入和不美观性。尽管只有一些编辑人员会考虑代码的外观,但每个用户却都可能遇到不作任何检测的脚本的随机的和不可预期的输出。尽管对自己的CGI 脚本中每个特定函数的成功与否采取什么态度似乎是一件小事,但却在很大程序上影响自己采取什么样错误处理方式。如果假定都会出错一通过检测调用的失败——脚本就能给自己和用户都发出更详细的错误信息。这样的小事正是区分成功的CGI程序和一个普通的CGI程序的关键。 3.错误报告 在成功地检测出错误后,就必须让用户知道是什么出错了以及他们如何才能解决。至少应该告诉他们如果他们自己不能解决问题,应该去找谁联系解决。 错误报告很重要,但是许多CGI 编程编程人员却忽略了这点。他们简单地检测出了错误并给用户提供了含糊的、可能毫无意义的错误消息,使得用户在大多数情况下只好耸耸肩转向另一个不那么让人糊涂的站点。如这样的一条消息: Something went wrong! 对用户没有什么帮助。 如何报告错误很关键,尽管有人说准确地检测出错误是错误处理两部分中重要的,但如何将错误消息返回给用户也不应忽视。 3.1 错误外观和保持一致的重要性 在编写CGI脚本的代码时,开始的想法可能是当出错时简单地扔给用户一个应答就行了。毕竟在编码时,注意力不是集中于程序正常运行会怎样,而不是不正常运行时会怎样。不过这种方式最终会降低脚本的效率。给用户发送不同的多条消息只会使他们更糊涂了。如果他们收到了错误消息,可能已经相当糊涂了。 错误消息的一致性与用户界面的易于使用性等方面的一致性一样重要。应尽可能一致地显示错误消息以便能很容易识别出来。 在错误报告中获得一致性的一个很好的办法是用一个子程序显示所有错误。通过接收关于每个错误的特殊信息该子程序按一个通用的清楚的外观包装每个错误。程序清单4 即是一个显示一致的错误的per1子程序。 程序清单4显示有一致外观的错误 sub error_Fatal { # Print the error print("\n"; print("

Error !

\n"); print("


@_


\n"); prtnt("\n"); # And exit exit(-1); } 当CGI脚本到达某个不能继续运行的点时,它就调用error_Fatal(),并用原因作为一个参数。error_Fatal()据此显示头标和头信息,然后显示错误。 警告 在编写错误子程序时,必须包括退出该CGI脚本的代码。如果漏了这个明显的但 却很容易忘记的步骤,那么子程序返回而程序继续执行一—导致出现其他问题。 不管使用error-Fatal() 的程序报告了多少不同的错误,它的外观都一样,从而当发生错误时用户立刻就能知道。 在创建了自己的子程序之后,可以给它加入一些东西充实它的内容,或者一些希望除公共的布局之外实际发给用户的信息。 3.2 简单的拒绝 给用户提供的最简单的错误是拒绝,仅仅告诉他们出错了而CGI 脚本不能继续运行了。程序清单5中的代码即报告了这种错误消息不能提供多少帮助。 程序清单5 简单的拒绝 Sub error_Fatal { # Print the error print "\n"; print "

Error !

\n"; print "


Something went wrong! I didn't expect"; print "that to heppen ! Huh!


\n"; prtnt "\n"; exit (-1); } 尽管自己可能想简单地告诉用户出现问题了,但却使用户糊里糊涂,甚至可能生气。如果确实出现了错误,他们希望知道他们能做什么以及如何做才能改正错误,至少错误消息应该给用户提供一个解释。 3.3 细节提示 当用户碰到错误消息时,他们应该立刻知道他们(或机器)出错了。一致的错误屏幕能帮助达到这个目的。 但是用户却不太清楚该屏出现的原因。如果用户意识到他们错了,他们可能是第一次犯这样的错,这样CGI脚本就不应该拒绝他们。这是整个脚本使用一种错误响应的基本问题。 编程人员应给用户提供错误的某些解释——它的原因,可能的影响以及如何纠正它,而不是简单地扔给用户一个通用的错误,让他们自己去找出原因。给错误条件提供解释不仅能帮助用户更容易纠正他们错误的输入——如果那是出错原因的话一对编程人员自己也有好处,他可以查清不同的错误。一个简单的描述性的错误消息可能会指出一个要花几小时的调试才能发现的问题。 程序清单6 即是程序清单5 的改进。子程序接收错误原因作为参数,然后将它们作为消息的一部分显示。 Sub error_Fatal { # Print the error print("\n"); print("

Error !

\n"); print("


@_!


\n"); prtnt("

\n"); exit(-1); } 尽管错误原因的解释是该子程序的一个很好的增加部分,最好还能增加一些方式以提供更多的细节。例如,程序清单6 没有提供一个描述性的标题。可以重写该子程序使它不仅接收错误的解释,还有一个标题。该标题更好地解释问题。程序清单7即是一个可能的方案。 程序清单7 显示更多信息的错误程序 sub error_Fatal { local($error_Title); # Get the specifics $error_Title = "General" unless $erroe_Title = $_[0]; # Print the error print("\n"); print("

Error: $error_Title !

\n"); print("


@_!


\n"); prtnt("

\n"); exit(-1); } 除错误的解释之外,该段代码还接收错误页面的标题。该页面可以提供更大的灵活性来报告脚本中运行出错的原因。注意也可以用空串作为标题,这就成了通常的缺省形式。如果可能的话,有缺省值总是方便一些。 当然,对于传递给用户的信息数量没有什么限制。当前时间、机器的平均负载、重要的数据库的大小——都是可以加入页面的,有些信息对用户是很有帮助,有些却没有。总而言之,应该传送给用户的是用户需要用来明白自己做错了什么的所有信息——如果实际上错误是由他们引起的话。太多的信息会使他们犯糊涂,而太少的信息又会使他们只好猜测原因弄得很烦躁。还应记住并不仅限于传递英文;也可以同样容易地给错误子程序发送HTML。解释一定要富于描述性并且要很详细,但不能太专业化。 3.4 管理联系、帮助指针 大家还可以给自己的错误页面再增加两项内容:管理联系和帮助指针,一般情况下,当某个web站点出错时最先注意到的是该站点的用户。除非自己能每天24小时,每周7天地监视自己计算机,浏览页面的人一般都会在管理员之前找到那些丢失的文件、破坏的数据库或者CGI错误。他能在发现出错时帮助编程人员,但是仅在编程人员帮助他们这么做时才行。 在浏览web 时,用户一般都会花几分重视艰险Web的管理者发一封关于错误的Email。 程序清单8 允许进行反馈的错误子程序 sub error_Fatal { 1ocal ($error_Title); 1ocal ($error_Mail); # Get the specifics $error_Title = "General" unless $error_Title = $_[0]; $error_Mail = "webmaster@www.server.com" unless $error_Mail=$_[1]; # Print the error print ""; print ""; print "\n"; print "

Error:$error_Title

\n"; print "


@_


\n"; print "please inform"; print "$error_Mail"; print "of this problem. Thank you.\n"; print "\n"; # Exit the program exit (-1); } 注意这是允许信息发给该子程序,然后再传给用户。现在该程序接收一个错误标题,一个报告问题的邮寄地址,以及实际错误本身的描述。 记住,用户实际上不欠自己什么东西所以应该让他们尽可能容易地报告有关自己的站点的问题。如果能在错误页面上增加管理联系事情就不一样了。 但是如何错误的原因是用户的错怎么办?如果用户仅仅是没有输入某个字段或是在e-mail地址中包含了未申明的指针,要求他们与Web 拥有者进行联系可能就不太适宜。一种解决方案是用对某个帮助文件的引用替换报告错误的请求,该帮助文件能帮助用户理解他们犯的错误。也就是说,如果用户犯了错误,告诉他们如何纠正好了。 程序清单9是对error_Fatal()的是一步改进。它接收一个URL页面和error-mai1地址,并将它作为帮助链接。 程序清单9 允许访问帮助的错误例程 sub error_Fatal { 1ocal ($error_Title); 1ocal ($error_Url); # Get the specifics $error_Title = "General" unless $error_Title = $_[0]; $error_Url = "http://......../help.htm" unless $error_Url=$_[1]; # Print the error print ""; print ""; print "\n"; print "

Error:$error_Title

\n"; print "


@_[2..@_]


\n"; print "For help, clickhere"; print "\n"; # Exit the program exit (-1); } 当然,可以将这两种技术组合进一个子程序中,或者跟好的办法是将error_Fatal() 子程序拆成两个———个用于系统错误(假定为error_System()),另一个用于用户错误(error_User())。 但是无论选择怎么做,都应记住管理联系和指向帮助文件的指针都是使自己的错误屏幕让用户那么讨厌的工具。给用户提供一些下一步该干什么的步骤提示,能让他们热衷于使用自己的站点。 3.5 导航帮助 指向Web拥有者的MAILTO 和指向帮助页面的HREF都是导航帮助。当用户碰到错误时不应碰到一堵墙,而应允许他们很容易地走出这一步。 但这些步骤都不能到达错误本身或者允许用户立刻跳回错误发生之前的地方。当然,几乎所有浏览器都会有一个Back按钮允许浏览者返回前一页。但该按钮可能隐藏起来了以致很难找到。增加一个链接不仅能使返回及重试方便得多,而且增加了最终的效果。 程序清单10 拥有返回链接的错误子程序 sub error_User { 1ocal ($error_Title); 1ocal ($error_UrlHelp); 1ocal ($error_UrlBack); # Get the specifics $error_Title = "General" unless $error_Title = $_[0]; $error_UrlHelp = "http://......../help.htm" unless $error_UrlHelp=$_[1]; $error_UrlBack = $ENV{"HTTP_REFERER"}; # Print the error print ""; print ""; print "\n"; print "

Error:$error_Title

\n"; print "


@_[2..@_]


\n"; if ($error_UrlBack) { print "To try again, click"; print "here."; } print "For help, clickhere"; print "\n"; # Exit the program exit (-1); } 该程序仍然接收一个标题和一个帮助 URL,不过它还利用HTTP_REFERER环境变量来获得前一页面的URL。该URL被用于允许用户简单地返回并重试。不过,如果HTTP_REFERER没有设置——换句话说,如果服务器不给CGI 脚本提供该信息——该行即被跳过以免给用户提供一条无用的链接。 注意 在编写CGI脚本时,千万不要给用户提供一个空的、无用的链接。尽管这些链接看起来很正常,当用户单击它们时什么也不发生,但定会使用户迷惑甚至觉得讨厌。在显示前请一定验证一下自己的数据。 程序清单10的子程序称为error_User(),因为它是在发生用户错误时调用的。如果系统产生了错误——比如找不到所需的文件——就不会希望用户能返回前一页面。如果某文件不可用,而用户能很容易重复这个导致第一次进入错误子程序的动作,他们会什么也得不到。一般情况下限于在由用户输入导致的错误屏幕上使用返回链接。 4 常见的错误 尽管CGI 脚本中能出现成千上万种不同的错误,但却只有一小部分是经常出现的。知道这些问题才能检测它们,而且基于它们的解决方法还能发现捕获其它错误的方法。 4.1 用户错误 因为用户只能按有限几种方式与CGI 脚本打交道——表单、图像映像、路径——所以应集中在这几个方面检测用户错误。如果某个用户犯了错误,总可以将它追踪到这些输入方法之一。 在接收用户输入时要做的第一件事就是验证它。尽管浏览自己站点的用户可能没有什么恶意,但他们却有无数多种方式可能在输入中出现小问题。 在接收用户的数据时,必须对它进行一些测试以保证是预期的值。如果用户在某个表单中提交了数据而预期的却是另一个,用户就可能(有意识地或无意识地)给web 站点造成不可估计的损害。 4.2 系统错误 在确认用户的所有输入都正确之后(或尽可能确认后),就应该处理系统自己可能产生的错误了。程序中包含的每个函数调用都可能返回错误,因为每个函数调用都可能以不同的方式出错。 应该仔细地检查使用的每个系统调用,看看是否存在问题。打开一个文件而不能确保一切都如预期的一样是很愚蠢的,最终会给自己和用户造成很多麻烦。读文件,写文件,搜索、拷贝、删除或移动文件而没有检测操作成功与否也很愚蠢。对任意文件,或系统重要部分做任何操作而不检查是否出错都是很蠢的。 不过确实存在例外。不小心发生问题的时候正好是没对问题进行处理的时候。例如,忽视Close() 调用的返回状态就很常见,因为如果该函数失败,就不再有资源了。即使是向屏幕幕发送文本的例程一—print()、Printf()、echo 或其他——也返回一个成功与否的值,但几乎没人去检查它。如果它真的出问题怎么办?打印一些信息吗? 不过一般说来,检查自己使用的每个系统调用是否出错很重要。在清除了CGI 脚本本身的错误和用户的不正确的输入之后,程序失败的唯一时间就是与系统交互时。当已经快要成功时却让问题溜掉真是让人不好意思。 4.3 自己的错误 当然应该记住自己的程序也是"系统"的一部分。自己编写的一个取几个数的平均值这样的程序没准也会和0pen()一样容易遇到错误,这样该程序在报告错误时就会像系统函数一样。 另外,和系统函数一样,也应检查自己的子程序可能返回的错误。指望自己的程序总是正常工作就象指望操作系统提供的例程总正常一样过于乐观,可能会导致同样多的问题。应确保在完整地检查错误时也包括自己的子程序。 5 错误处理的原则 除了检查来自系统和用户特定错误之外,处理错误时应记住的最重要的一件事是弄清楚自己在干什么。因为错误处理不是自己CGI 脚本的重点,所以经常很容易形成坏习惯或粗心大意。 应该时刻警惕这种倾向;用所有可能的方法避免它,在编写代码时向自己提问,写完后重读一遍程序,甚至可以建立测试环境以导致特定的错误;以保证程序能如预期进行报告。简短他说,采取什么样的错误处理的原则一如何检查并报告错误——与程序的其他方面一样重要。 5.1 完整性 在检查错误时,不要对某个函数的执行结果过于乐观,假定什么地方出错了,然后检查是不是这样——在尽可能多的地方按尽可能不同的方式去做。 计算机是比较挑剔的,最小的错误也会给程序返回一个错误,即使是最小的细节不是如预期的一样,整个CGI 脚本也会破坏。如果检查到了错误,就可以给用户提供一条错误消息并指示下一步该干什么。请记住Murphy法则。未检查错误的代码处将是产生最多错误的地方,所以每个地方都要检查。 5.2 详细性 当程序确实检查到一个错误时——最终会查到的——应该尽可能详细地描述它。如果没有提供足够多的信息,有错误消息几乎和没有错误消息一样。 提示 编完程序后,最好能回过头来重读一遍自己的错误消息。在编码时看起来很有道理的语句也许由于现在处在缺乏技术的状态下变得毫无意义了。 记住,用户可能会比自己更缺乏专业知识。在解释问题时不要过于简单,要保证能让他们理解。一定要用平实的语言,要有针对性;避免专业术语和技术词汇。 当然,如果错误不是用户的错误,如果是由自己的脚本或web 服务器产生的问题,就要使自己的错误信息包含尽可能多的信息。不仅要涉及发生什么错误,还应涉及在什么地方出错以及错误的内容,这种详细的信息对调试问题会有很大的帮助。实际上,许多语言都提供了工具可以很容易报告某个错误的内容。现在的C编译器就允许使用宏_LINE_,_FILE_和_DATE_来标识产生错误的语句行和文件,以及编译的时间和日期。error 全局变量包含某个系统调用失败的原因,而perror()则能将这些代码翻译成英文。