Gérer proprement les erreurs avec ASP [HOW TO] - ASP - Programmation
MarshPosté le 22-07-2004 à 15:35:48
Je suis en train de réécrire un site e-Commerce pour une grande société. Pour un nombre incalculable de raisons liées aux données récupérées de leur système d'information, il se produit régulièrement des erreurs sur leur site actuel, y compris dans des routines critiques du site, ce qui constitue souvent des problèmes graves de cohérence des données (une fois on est allé jusqu'à avoir un client qui a payé pour la commande d'une autre personne), sans que personne ne s'en rendre compte (ce n'est qu'un mois plus tard qu'on s'est rendu compte de l'erreur, lorsque le client qui avait payé la commande s'est étonné auprès du call-center de ne rien avoir reçu.
Je mets donc un point d'honneur sur cette nouvelle version pour faire un système de gestion des erreurs qui nous permette de gérer proprement les erreurs, notamment être prévenu sur le champ avec un message suffisament détaillé pour retrouver où s'est produite l'erreur, et pourquoi.
Je tiens à vous faire part des différentes techniques que j'utilise, et attends de vous un feed-back, ou des idées pour les améliorer.
1) Les transactions. Evidement, c'est la première chose qu'il faut faire pour garantir l'intégrité des données dans la base... Par expérience, c'est pourtant rarement utilisé en ASP (et sur le web en général).
Ci-dessous un exemple de routine permettant de garantir l'intégrité des données lors de l'inscription d'un utilisateur :
sub getRegisterStep4() cnx.beginTrans on error resume next
dim sqlNewId, newId sqlNewId = "select seq_tie.nextval newid from dual" dim rsNewId set rsNewId = Server.CreateObject("ADODB.RecordSet" ) set rsNewId.ActiveConnection = cnx rsNewId.Open sqlNewId newId = rsNewId("newid" ) rsNewId.Close Set rsNewId = Nothing
dim sqlPay sqlPay = "select etbcod, posfis, typcde from pay where codpay = " & Quote(Request.Form("codpay" )) dim rsPay set rsPay = Server.CreateObject("ADODB.RecordSet" ) set rsPay.ActiveConnection = cnx rsPay.Open sqlPay
randomize dim strCHARS, strPASS, i strCHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" strPASS = "" for i = 1 to 8 strPASS = strPASS & Mid(strCHARS, int(rnd(i) * 36) + 1, 1) next
dim news if Request.Form("news" ) <> "" then news = "Y" else news = "N" end if
if err = 0 then on error goto 0 cnx.CommitTrans call registerMail(Request.Form("contact1" ), Request.Form("contact2" ), newId, strPASS, Request.Form("codlan" ), Request.Form("email" )) dim sql sql = "select desc1 from news where typnew = 'REG' and codlan = " & Quote(codlan) & " and rank = 4" dim rs set rs = Server.CreateObject("ADODB.RecordSet" ) set rs.ActiveConnection = cnx rs.Open sql if not rs.EOF then Response.Write "<p>" & rs("desc1" ) & "</p>" end if rs.Close set rs = Nothing else Response.Write getLabel("errorOccured", codlan) cnx.RollbackTrans on error goto 0 end if end sub
Il faut regarder tout en haut le cnx.beginTrans cnx est mon objet connection : Set cnx = Server.CreateObject("ADODB.Connection" ) Si le SGBD utilisé supporte les transactions, alors cela va en créer une sur le SGBD. Ici j'utilise Oracle, donc aucun problème de ce côté. Le "on error resume next" juste en dessous peut vous sembler bien crade... Hé oui, mais si on veut que la transaction fonctionne correctement (et récupérer l'info comme quoi il y a eu une erreur), c'est le seul moyen.
On effectue les traîtements de création de l'utilisateur.
Puis, on teste si err = 0 err est un objet mis à jour lorsqu'il se produit une erreur, et que la gestion des erreurs est active (on error resume next) Si sa propriété par défaut est égale à 0, alors il n'y a pas eu d'erreur. On peut alors faire le cnx.commitTrans qui va avoir pour effet de valider la transaction. Sinon, le cnx.rollbackTrans permet d'annuler toutes les requêtes de la transaction dans la base. Ainsi, on ne se retrouve pas avec des bouts de client créés un peu partout dans la base avec des bouts qui manquent, même si une erreur se produit en plein milieu.
Attention, il faut IMPERATIVEMENT faire l'une ou l'autre de ces deux actions (d'où l'obligation d'utiliser un on error resume next pour ne pas s'arrêter en cas d'erreur). En effet, sinon la transaction sera automatiquement rollbackée... mais au bout de très longues minutes, au moment du timeout associé à la transaction (ca peut durer des jours). Hors une transaction peut générer des locks et surtout un espace mémoire utilisé très important, il faut donc s'en débarrasser le plus rapidement possible, ...et proprement !
2) Envois d'un mail de notification d'erreur, avec des données utiles : Juste avant le rollBack, j'ai en fait cette ligne dans mon code :
Cela me permet d'envoyer un mail avec les trois requêtes qui ont été éxécutée au moment de l'erreur. Mais ce n'est pas tout. Voici ma fonction SendErrorMail() :
Ca génère ce type de mail à chaque erreur (lorsqu'on a mis l'appel à la fonction à l'endroit où ça a planté ) [quote]Error:
Devise introuvable dans la table DEV : 'FRA'
sigtie = 520036 typtie = CLI login = 520036 password = MonPass coddev = FRA codlan = FRA
AUTH_TYPE : CONTENT_TYPE : CONTENT_LENGTH : 0 DOCUMENT : DOCUMENT_URI : DATE_GMT : DATE_LOCAL : GATEWAY_INTERFACE : CGI/1.1 HTTP_USER_AGENT : Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.1.4322) HTTP_REFERER : http://localhost/catalog.asp?famweb=M&sfaweb=SCA HTTP_COOKIE : codlan=ENG; user%5Fcodlan=FRA; password=MonPass; sigtie=520036; typtie=CLI; coddev=FRA; login=520036 LAST_MODIFIED : LOGON_USER : PATH_INFO : /catalog.asp PATH_TRANSLATED : D:\docs\Bureau\e-Commerce-Dev\catalog.asp REMOTE_ADDR : 127.0.0.1 REMOTE_HOST : 127.0.0.1 REMOTE_IDENT : REMOTE_USER : REQUEST_METHOD : GET SCRIPT_MAP : SCRIPT_NAME : /catalog.asp SERVER_NAME : localhost SERVER_PORT : 80 SERVER_PORT_SECURE : 0 SERVER_PROTOCOL : HTTP/1.1 SERVER_SOFTWARE : Microsoft-IIS/5.0 URL : /catalog.asp[/citation] On voit que toutes les variables d'environnement serveur sont récupérées automatiquement, afin notamment de savoir ce qui est passé en post, en get, l'url utilisé, la page précédement visitée, le protocole de connection, le nom de la page qui a planté, la version du navigateur, etc.
Ensuite, un certain nombre de variables globales sont présentes, notamment tout ce qui concerne le profil du client (login/pass/compte/langue/devise)
Tout ceci afin de pouvoir reproduire le bug sans avoir à demander au client (quand on sait qui c'est) ce qu'il a fait, car il est généralement bien incapable de dire ce qu'il a fait, et encore moins ce qu'il désirait faire... Il se sent tellement coupable d'avoir fait planter le serveur qu'il n'ose pas dire un mot, de peur de se faire engueuler (le pire c'est que c'est vrai)
Voilà.
J'aimerais savoir ce que vous en pensé, et j'espère que ca pourra aider ceux qui ne savent pas comment faire le minium.
J'ai pensé aussi à une autre chose, au lieu de faire des rs.Open pour éxécuter une fonction, utiliser une fonction "getRs(sql)" qui retourne le rs ouvert lorsque ça a marché, ou envoie un mail en cas d'erreur.
Mais à ce moment, j'ai peur de ne pas trop savoir quoi faire en cas d'erreur (arrêt de l'appli sur cette requête, ou redonner la main à l'appli générale pour qu'elle traîte l'erreur elle-même ?)
Marsh Posté le 22-07-2004 à 15:35:48
Je suis en train de réécrire un site e-Commerce pour une grande société.
Pour un nombre incalculable de raisons liées aux données récupérées de leur système d'information, il se produit régulièrement des erreurs sur leur site actuel, y compris dans des routines critiques du site, ce qui constitue souvent des problèmes graves de cohérence des données (une fois on est allé jusqu'à avoir un client qui a payé pour la commande d'une autre personne), sans que personne ne s'en rendre compte (ce n'est qu'un mois plus tard qu'on s'est rendu compte de l'erreur, lorsque le client qui avait payé la commande s'est étonné auprès du call-center de ne rien avoir reçu.
Je mets donc un point d'honneur sur cette nouvelle version pour faire un système de gestion des erreurs qui nous permette de gérer proprement les erreurs, notamment être prévenu sur le champ avec un message suffisament détaillé pour retrouver où s'est produite l'erreur, et pourquoi.
Je tiens à vous faire part des différentes techniques que j'utilise, et attends de vous un feed-back, ou des idées pour les améliorer.
1) Les transactions.
Evidement, c'est la première chose qu'il faut faire pour garantir l'intégrité des données dans la base... Par expérience, c'est pourtant rarement utilisé en ASP (et sur le web en général).
Ci-dessous un exemple de routine permettant de garantir l'intégrité des données lors de l'inscription d'un utilisateur :
sub getRegisterStep4()
cnx.beginTrans
on error resume next
dim sqlNewId, newId
sqlNewId = "select seq_tie.nextval newid from dual"
dim rsNewId
set rsNewId = Server.CreateObject("ADODB.RecordSet" )
set rsNewId.ActiveConnection = cnx
rsNewId.Open sqlNewId
newId = rsNewId("newid" )
rsNewId.Close
Set rsNewId = Nothing
dim sqlPay
sqlPay = "select etbcod, posfis, typcde from pay where codpay = " & Quote(Request.Form("codpay" ))
dim rsPay
set rsPay = Server.CreateObject("ADODB.RecordSet" )
set rsPay.ActiveConnection = cnx
rsPay.Open sqlPay
dim sqlINS
sqlINS = "insert into tie (CODSOC, TYPTIE, SIGTIE, NOMTIE, CODPAY, CODDEV, TYPCDE, SIGREP, SIGGRP, POSFIS, CODBAR, ETBCOD, FAMTIE, MODRGL, CODDPT, CODQUA, CODSPE) " &_
"values (0, 'CLI', " & Quote(newId) & ", " & Quote(Request.Form("NOMTIE" )) & ", " & Quote(Request.Form("CODPAY" )) & ", " & Quote(Request.Form("CODDEV" )) & ", " & Quote(rsPay("typcde" )) & ", 'WEB" & rsPay("etbcod" ) & "', ' ', " & Quote(rsPay("posfis" )) & ", ' ', " & Quote(rsPay("etbcod" )) & ", " & Quote(Request.Form("FAMTIE" )) & ", 'CB', '@', 0, " & Quote(Request.Form("CODSPE" )) & " )"
cnx.Execute sqlINS
rsPay.Close
Set rsPay = Nothing
dim contact
if Len(Trim(Request.Form("contact1" )) & " " & Trim(Request.Form("contact2" ))) <= 20 then
contact = Trim(Request.Form("contact1" )) & " " & Trim(Request.Form("contact2" ))
else
contact = Mid(Trim(Request.Form("contact2" )), 1, 20)
end if
dim sqlADR
sqlADR = "insert into adr (CODSOC, TYPTIE, SIGTIE, TYPADR, NUMADR, LIBADR, ADRESS, ADRSUI, LOCALI, CODPOS, CENPOS, CODPAY, CONTACT, TEL, FAX, COMMEN1, COMMEN2, COMMEN3, COMMEN4, COMMEN5, ADREDI, CODCIV) " &_
"values (0, 'CLI', " & Quote(newId) & ", 'COM', 800, " & Quote(Request.Form("nomtie" )) & ", " & Quote(Request.Form("adress" )) & ", nvl(" & Quote(Request.Form("adrsui" )) & ", ' '), nvl(" & Quote(Request.Form("locali" )) & ", ' '), " & Quote(Request.Form("codpos" )) & ", " & Quote(Request.Form("cenpos" )) & ", " & Quote(Request.Form("codpay" )) & ", " & Quote(contact) & ", " & Quote(Request.Form("tel" )) & ", nvl(" & Quote(Request.Form("fax" )) & ", ' '), ' ', ' ', ' ', ' ', ' ', " & Quote(Request.Form("email" )) & ", " & Quote(Request.Form("codciv" )) & " )"
cnx.Execute sqlADR
randomize
dim strCHARS, strPASS, i
strCHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
strPASS = ""
for i = 1 to 8
strPASS = strPASS & Mid(strCHARS, int(rnd(i) * 36) + 1, 1)
next
dim news
if Request.Form("news" ) <> "" then
news = "Y"
else
news = "N"
end if
dim sqlUSR
sqlUSR = "insert into usr (CODSOC, LOGIN, PASSWORD, NOM, PRENOM, CODLAN, CODDEV, TYPTIE, SIGTIE, EMAIL, NEWS, ACTIF) " &_
"values (0, " & Quote(newId) & ", " & Quote(strPASS) & ", " & Quote(Trim(Request.Form("contact1" ))) & ", " & Quote(Trim(Request.Form("contact2" ))) & ", " & Quote(Request.Form("codlan" )) & ", " & Quote(Request.Form("coddev" )) & ", 'CLI', " & Quote(newId) & ", " & Quote(Request.Form("email" )) & ", " & Quote(news) & ", 'Y')"
cnx.Execute sqlUSR
if err = 0 then
on error goto 0
cnx.CommitTrans
call registerMail(Request.Form("contact1" ), Request.Form("contact2" ), newId, strPASS, Request.Form("codlan" ), Request.Form("email" ))
dim sql
sql = "select desc1 from news where typnew = 'REG' and codlan = " & Quote(codlan) & " and rank = 4"
dim rs
set rs = Server.CreateObject("ADODB.RecordSet" )
set rs.ActiveConnection = cnx
rs.Open sql
if not rs.EOF then
Response.Write "<p>" & rs("desc1" ) & "</p>"
end if
rs.Close
set rs = Nothing
else
Response.Write getLabel("errorOccured", codlan)
cnx.RollbackTrans
on error goto 0
end if
end sub
Il faut regarder tout en haut le cnx.beginTrans
cnx est mon objet connection : Set cnx = Server.CreateObject("ADODB.Connection" )
Si le SGBD utilisé supporte les transactions, alors cela va en créer une sur le SGBD. Ici j'utilise Oracle, donc aucun problème de ce côté.
Le "on error resume next" juste en dessous peut vous sembler bien crade... Hé oui, mais si on veut que la transaction fonctionne correctement (et récupérer l'info comme quoi il y a eu une erreur), c'est le seul moyen.
On effectue les traîtements de création de l'utilisateur.
Puis, on teste si err = 0
err est un objet mis à jour lorsqu'il se produit une erreur, et que la gestion des erreurs est active (on error resume next)
Si sa propriété par défaut est égale à 0, alors il n'y a pas eu d'erreur.
On peut alors faire le cnx.commitTrans qui va avoir pour effet de valider la transaction.
Sinon, le cnx.rollbackTrans permet d'annuler toutes les requêtes de la transaction dans la base. Ainsi, on ne se retrouve pas avec des bouts de client créés un peu partout dans la base avec des bouts qui manquent, même si une erreur se produit en plein milieu.
Attention, il faut IMPERATIVEMENT faire l'une ou l'autre de ces deux actions (d'où l'obligation d'utiliser un on error resume next pour ne pas s'arrêter en cas d'erreur). En effet, sinon la transaction sera automatiquement rollbackée... mais au bout de très longues minutes, au moment du timeout associé à la transaction (ca peut durer des jours). Hors une transaction peut générer des locks et surtout un espace mémoire utilisé très important, il faut donc s'en débarrasser le plus rapidement possible, ...et proprement !
2) Envois d'un mail de notification d'erreur, avec des données utiles :
Juste avant le rollBack, j'ai en fait cette ligne dans mon code :
call SendErrorMail(sqlINS & "<br><br>" & sqlADR & "<br><br>" & sqlUSR)
Cela me permet d'envoyer un mail avec les trois requêtes qui ont été éxécutée au moment de l'erreur.
Mais ce n'est pas tout. Voici ma fonction SendErrorMail() :
sub SendErrorMail(errorMessage)
dim objMail
Set objMail = Server.CreateObject("CDONTS.NewMail" )
objMail.Subject = "## WEBSITE ERROR ##"
objMail.From = "errormanager@gemedicalsystems.com"
objMail.To = getAlphaNumericParameter("IT_TEAM" )
objMail.Cc = ""
objMAIL.Bcc = ""
objMail.Body = "An error occured on the page " & Request.ServerVariables("SCRIPT_NAME" ) & "<br><br>Error:<br>" & err.Source & "<br>" & err.Description & "<br><br>" &_
errorMessage & "<br><br>" &_
"sigtie = " & sigtie & "<br>" &_
"typtie = " & typtie & "<br>" &_
"login = " & login & "<br>" &_
"password = " & password & "<br>" &_
"coddev = " & coddev & "<br>" &_
"codlan = " & codlan & "<br>" &_
"<br>" &_
"AUTH_TYPE : " & Request.ServerVariables("AUTH_TYPE" ) & "<br>" &_
"CONTENT_TYPE : " & Request.ServerVariables("CONTENT_TYPE" ) & "<br>" &_
"CONTENT_LENGTH : " & Request.ServerVariables("CONTENT_LENGTH" ) & "<br>" &_
"DOCUMENT : " & Request.ServerVariables("DOCUMENT" ) & "<br>" &_
"DOCUMENT_URI : " & Request.ServerVariables("DOCUMENT_URI" ) & "<br>" &_
"DATE_GMT : " & Request.ServerVariables("DATE_GMT" ) & "<br>" &_
"DATE_LOCAL : " & Request.ServerVariables("DATE_LOCAL" ) & "<br>" &_
"GATEWAY_INTERFACE : " & Request.ServerVariables("GATEWAY_INTERFACE" ) & "<br>" &_
"HTTP_USER_AGENT : " & Request.ServerVariables("HTTP_USER_AGENT" ) & "<br>" &_
"HTTP_REFERER : " & Request.ServerVariables("HTTP_REFERER" ) & "<br>" &_
"HTTP_COOKIE : " & Request.ServerVariables("HTTP_COOKIE" ) & "<br>" &_
"LAST_MODIFIED : " & Request.ServerVariables("LAST_MODIFIED" ) & "<br>" &_
"LOGON_USER : " & Request.ServerVariables("LOGON_USER" ) & "<br>" &_
"PATH_INFO : " & Request.ServerVariables("PATH_INFO" ) & "<br>" &_
"PATH_TRANSLATED : " & Request.ServerVariables("PATH_TRANSLATED" ) & "<br>" &_
"REMOTE_ADDR : " & Request.ServerVariables("REMOTE_ADDR" ) & "<br>" &_
"REMOTE_HOST : " & Request.ServerVariables("REMOTE_HOST" ) & "<br>" &_
"REMOTE_IDENT : " & Request.ServerVariables("REMOTE_IDENT" ) & "<br>" &_
"REMOTE_USER : " & Request.ServerVariables("REMOTE_USER" ) & "<br>" &_
"REQUEST_METHOD : " & Request.ServerVariables("REQUEST_METHOD" ) & "<br>" &_
"SCRIPT_MAP : " & Request.ServerVariables("SCRIPT_MAP" ) & "<br>" &_
"SCRIPT_NAME : " & Request.ServerVariables("SCRIPT_NAME" ) & "<br>" &_
"SERVER_NAME : " & Request.ServerVariables("SERVER_NAME" ) & "<br>" &_
"SERVER_PORT : " & Request.ServerVariables("SERVER_PORT" ) & "<br>" &_
"SERVER_PORT_SECURE : " & Request.ServerVariables("SERVER_PORT_SECURE" ) & "<br>" &_
"SERVER_PROTOCOL : " & Request.ServerVariables("SERVER_PROTOCOL" ) & "<br>" &_
"SERVER_SOFTWARE : " & Request.ServerVariables("SERVER_SOFTWARE" ) & "<br>" &_
"URL : " & Request.ServerVariables("URL" )
objMail.BodyFormat = 0
objMail.MailFormat = 0
objMail.Send
set objMail = nothing
end sub
Ca génère ce type de mail à chaque erreur (lorsqu'on a mis l'appel à la fonction à l'endroit où ça a planté )
[quote]Error:
Devise introuvable dans la table DEV : 'FRA'
sigtie = 520036
typtie = CLI
login = 520036
password = MonPass
coddev = FRA
codlan = FRA
AUTH_TYPE :
CONTENT_TYPE :
CONTENT_LENGTH : 0
DOCUMENT :
DOCUMENT_URI :
DATE_GMT :
DATE_LOCAL :
GATEWAY_INTERFACE : CGI/1.1
HTTP_USER_AGENT : Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.1.4322)
HTTP_REFERER : http://localhost/catalog.asp?famweb=M&sfaweb=SCA
HTTP_COOKIE : codlan=ENG; user%5Fcodlan=FRA; password=MonPass; sigtie=520036; typtie=CLI; coddev=FRA; login=520036
LAST_MODIFIED :
LOGON_USER :
PATH_INFO : /catalog.asp
PATH_TRANSLATED : D:\docs\Bureau\e-Commerce-Dev\catalog.asp
REMOTE_ADDR : 127.0.0.1
REMOTE_HOST : 127.0.0.1
REMOTE_IDENT :
REMOTE_USER :
REQUEST_METHOD : GET
SCRIPT_MAP :
SCRIPT_NAME : /catalog.asp
SERVER_NAME : localhost
SERVER_PORT : 80
SERVER_PORT_SECURE : 0
SERVER_PROTOCOL : HTTP/1.1
SERVER_SOFTWARE : Microsoft-IIS/5.0
URL : /catalog.asp[/citation]
On voit que toutes les variables d'environnement serveur sont récupérées automatiquement, afin notamment de savoir ce qui est passé en post, en get, l'url utilisé, la page précédement visitée, le protocole de connection, le nom de la page qui a planté, la version du navigateur, etc.
Ensuite, un certain nombre de variables globales sont présentes, notamment tout ce qui concerne le profil du client (login/pass/compte/langue/devise)
Tout ceci afin de pouvoir reproduire le bug sans avoir à demander au client (quand on sait qui c'est) ce qu'il a fait, car il est généralement bien incapable de dire ce qu'il a fait, et encore moins ce qu'il désirait faire... Il se sent tellement coupable d'avoir fait planter le serveur qu'il n'ose pas dire un mot, de peur de se faire engueuler (le pire c'est que c'est vrai)
Voilà.
J'aimerais savoir ce que vous en pensé, et j'espère que ca pourra aider ceux qui ne savent pas comment faire le minium.
J'ai pensé aussi à une autre chose, au lieu de faire des rs.Open pour éxécuter une fonction, utiliser une fonction "getRs(sql)" qui retourne le rs ouvert lorsque ça a marché, ou envoie un mail en cas d'erreur.
Mais à ce moment, j'ai peur de ne pas trop savoir quoi faire en cas d'erreur (arrêt de l'appli sur cette requête, ou redonner la main à l'appli générale pour qu'elle traîte l'erreur elle-même ?)