Accueil > Non classé > GWT = Google Won’t Throw ?

GWT = Google Won’t Throw ?

Connaissez-vous l’histoire de l’exception dont on a perdu trace ?

Vous le savez probablement, chaque service GWT-RPC est en réalité une servlet. Petit rappel :

  • les appels Ajax GWT-RPC sont réalisés en POST,
  • le nom de la méthode du service à appeler est spécifié dans la requête envoyée,
  • l’appel est traité dans la méthode doPost(HttpServletRequest request, HttpServletResponse response), qui se charge de désérialiser les paramètres, faire appel à la bonne méthode du service par réflexion, puis sérialiser la réponse.

Que se passe t’il lorsque la méthode du service jette une exception ? Deux possibilités :

  • soit l’exception est déclarée dans l’interface du service, et celle-ci sera sérialisée jusqu’au client, qui pourra la traiter correctement,
  • soit ce n’est pas le cas (y compris pour les RuntimeException), et le client recevra une erreur 500, et le tristement célèbre :

    The call failed on the server; see server log for details


Tout va pour le mieux dans le meilleur des mondes… jusqu’au jour où, didiou! on reçoit la fameuse erreur 500, mais sans que rien n’apparaissent dans les logs serveur. Avant de dégainer le fichier de conf log4j commons-logging logback javalogging, slf4j, sortons notre loupe pour comprendre ce que deviennent ces exceptions non déclarées.

Allons regarder du côté de la méthode doPost() de AbstractRemoteServiceServlet :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  /**
   * Standard HttpServlet method: handle the POST. Delegates to
   * {@link #processPost(HttpServletRequest, HttpServletResponse)}.
   *
   * This doPost method swallows ALL exceptions, logs them in the
   * ServletContext, and returns a GENERIC_FAILURE_MSG response with status code
   * 500.
   */

  @Override
  public final void doPost(HttpServletRequest request, HttpServletResponse response) {
    try {
      // [...]
      processPost(request, response);
    } catch (Throwable e) {
      // Give a subclass a chance to either handle the exception or rethrow it
      doUnexpectedFailure(e);
    } finally {
      // [...]
    }
  }

La javadoc est claire, doPost() avale toute exception qui pointerait le bout de son nez, et la log dans le servlet context. Allons vérifier dans doUnexpectedFailure() :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  /**
   * Override this method to control what should happen when an exception
   * escapes the {@link #doPost} method. The default implementation will log the
   * failure and send a generic failure response to the client.
   *

   * An "expected failure" is an exception thrown by a service method that is
   * declared in the signature of the service method. These exceptions are
   * serialized back to the client, and are not passed to this method. This
   * method is called only for exceptions or errors that are not part of the
   * service method's signature, or that result from SecurityExceptions,
   * SerializationExceptions, or other failures within the RPC framework.
   *

   * Note that if the desired behavior is to both send the GENERIC_FAILURE_MSG
   * response AND to rethrow the exception, then this method should first send
   * the GENERIC_FAILURE_MSG response itself (using getThreadLocalResponse), and
   * then rethrow the exception. Rethrowing the exception will cause it to
   * escape into the servlet container.
   *
   * @param e the exception which was thrown
   */

  protected void doUnexpectedFailure(Throwable e) {
    // [...]
    ServletContext servletContext = getServletContext();
    RPCServletUtils.writeResponseForUnexpectedFailure(servletContext, getThreadLocalResponse(), e);
  }

Bon, suivons la trace, du côté de writeResponseForUnexpectedFailure() :


1
2
3
4
5
6
  public static void writeResponseForUnexpectedFailure(ServletContext servletContext, HttpServletResponse response, Throwable failure) {
    servletContext.log("Exception while dispatching incoming RPC call", failure);

    // Send GENERIC_FAILURE_MSG with 500 status.
    // [...] Peu importe
  }

Bingo ! On a trouvé, on est content : servletContext.log(String, Throwable). Il ne nous reste plus qu’à configurer nos logs correctement.

End of story ?

Pas tout à fait…

Si vous avez fait attention (ou lu la javadoc, chenapan!), vous aurez remarqué que doPost() catch Throwable, lâche un ptit log, renvoie une petite erreur 500, puis… rien.

GWT = Google Won’t Throw ?

Le conteneur de servlet ne recevra jamais un seul Throwable. Et alors ? Alors, chez nos amis les Throwables, il n’y a pas que les exceptions, il y a aussi les Error. Vous connaissez probablement déjà l’OutOfMemoryError, ou sa petite cousine bien connue du monde du test, l’AssertionError. Et que dit la javadoc d’Error ?

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.

GWT, déraisonnable ?

Je sais pas vous, mais moi j’aime pas trop contrarier la Javadoc sans une bonne raison.

En avalant une Error, on empêche la JVM de s’arrêter et on la laisse dans un état instable : imaginez l’état d’une JVM ayant lancé une OutOfMemoryError mais qu’on forcerait à continuer ?! De même quid d’une ExceptionInInitializerError ? Cette erreur traduit une exception dans un bloc statique, ce qui empêche une classe de se charger. Or, de nombreux frameworks font appel à Class.forName() pour charger les classes de composants à créer, en Lazy.

Cela peut même poser problème dans le cas d’exceptions plus classiques, par exemple pour faire du push Ajax avec comet sur Jetty.

Heureusement, il est tout à fait possible de modifier ce comportement.

Let’s test it together

Créez un nouveau projet GWT :

File > New > Web Application Project, décochez Use Google App Engine, et cochez Generate GWT Sample Code.

Ouvrez la classe GreetingServiceImpl, et modifiez le contenu de la méthode greetServer() pour qu’elle jette une Error.


1
2
3
    public String greetServer(String input) throws IllegalArgumentException {
        throw new AssertionError();
    }

Testez, et vous verrez que l’Error est bien trappée, loguée, mais pas rejetée au conteneur.

Rejeter la faute

Comme le précise la javadoc, la solution est simple : il suffit d’overrider doUnexpectedFailure() et d’implémenter le comportement voulu.


1
2
3
4
5
6
7
8
9
10
    @Override
    protected void doUnexpectedFailure(Throwable e) {
        try {
            super.doUnexpectedFailure(e);
        } finally {
            if (e instanceof Error) {
                throw (Error) e;
            }
        }
    }

Notez qu’on utilise un bloc try{} finally {} pour éviter qu’une exception jetée dans la méthode parente ne cache l’Error, qui est plus importante.

Je pense que ce comportement devrait être celui par défaut de la méthode doUnexpectedFailure().
Et vous, qu’en pensez-vous ?

Share
  1. Psidoler
    17/09/2011 à 10:38 | #1

    Très bonne enquête ! Félicitations.
    Votre démonstration convainc le néophyte que je suis de la nécessité d’overwriter doUnexpectedFailure().

    A mon tour de vous poser une question : qu’est-ce qui se cache derrière votre : “Bingo ! On a trouvé, on est content : servletContext.log(String, Throwable). Il ne nous reste plus qu’à configurer nos logs correctement” ?
    Comment configurer nos logs justement ?

    J’aimerai beaucoup récupérer toutes ces exceptions en dehors du log par défaut du serveur (via Log4j si possible).

    Psidoler

  2. 17/09/2011 à 15:59 | #2

    Bonjour. Content que ça vous ait plus :-) .

    Pour la conf des logs, je n’ai pas voulu rentrer dans les détails parce que c’est assez variable, et un vaste sujet à débat.

    Pourquoi ne pas tout simplement overrider doUnexpectedFailure, pour y ajouter un log avec Log4 ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        @Override
        protected void doUnexpectedFailure(Throwable e) {
            try {
                super.doUnexpectedFailure(e);
            } finally {

                // Log de l'exception "e" avec Log4J, slf4j, ou autre.

                if (e instanceof Error) {
                    throw (Error) e;
                }
            }
        }
  3. 12/12/2011 à 16:19 | #3

    Omagad, ça fait 3 mois que je bataille avec les erreurs 500 de GWT, j’ai try-catché toutes mes méthodes de services pour afficher les exceptions avant qu’elles disparaissent, je trouvais ça moche mais au moins je pouvais débugger. Aujourd’hui, rien d’affiché, le code côté serveur marche, mais le client reçoit une erreur 500…

    Grâce à ta solution, j’ai pu catcher une magnifique SerializationException qu’il refusait de me montrer autrement. Je suis toujours dans la bataille, merci !

  4. Julien
    01/03/2013 à 19:05 | #4

    J’ai testé votre méthode.
    Côté client GWT, je ne vois pas de différence. C’est toujours le même message qui s’affiche :
    com.google.gwt.user.client.rpc.StatusCodeException: 500 The call failed on the server; see server log for details

    J’ai loupé qq chose ?

  5. Geoffroy
    06/10/2014 à 16:16 | #5

    Même fin 2014, cette page est toujours d’actualité et m’a permis d’attraper un NullPointerException invisible sans cela. Merci, maintenant j’ai à la fois l’erreur 500 qui demande d’aller voir le log serveur et le log serveur qui manquait.

  1. 07/06/2014 à 05:49 | #1
  2. 07/06/2014 à 06:00 | #2
  3. 07/06/2014 à 08:09 | #3