Overriding Methods with Non-Existent Methods

Ovid on 2008-06-26T15:57:44

Today I spent a few hours trying to nail down a nasty bug. Once I could replicate a small test case, what finally clued me in was an error message that looked impossible, but I remembered seeing it somewhere before. I just couldn't remember where. Consider this:

if ($c->user || $c->authenticate(undef, 'Pips Realm')) {

That was failing with "Undefined subroutine &Pips3::user called at ..."

Wait! What? I didn't call a subroutine. I called a method! What the hell is going on?

For our tests, we sometimes override the authenticated user in our Pips3 application. The core of this code looks like this:

sub new {
    my ( $class, $user ) = @_;

    unless (defined wantarray) {
        croak('PIPTest::User->new($user) must not be called in void context');
    }
    my $self = bless {
        pips_user => \&Pips3::user,
        cat_user  => \&Catalyst::Request::user,
        user      => $user,
    } => $class;

    $self->_set_user($user);
    return $self;
}

sub _set_user {
    my $self = shift;

    # Get reference to sub returning the user object
    my $coderef = $self->_return_user_coderef;

    # Override Pips3::user
    {
        no warnings 'redefine';
        *Pips3::user             = $coderef;
        *Catalyst::Request::user = $coderef;
    }

    # Return the user object for convenience
    return $coderef->();
}

sub DESTROY {
    my $self = shift;
    no warnings 'redefine';
    *Pips3::user             = $self->{pips_user};
    *Catalyst::Request::user = $self->{cat_user};
}

When you call 'new' with a valid user name, you get a token back and while it is in scope, you have overridden the user for the application. I was quite happy with this code, but there's a nasty, nasty bug in it. Once I remembered what it was, the fix was painfully obvious.

In Perl, when you try to pull a coderef out of a typeglob, you sometimes get some strange behavior:

sub Foo::bar {'foo'}

my $code1 = *Foo::bar{CODE};
my $code2 = *Foo::no_such_subroutine{CODE};
print "$code1 $code2";
__END__
# output is something like:

foo
CODE(0x8063d94) CODE(0x8063d70)

As you can see, you apparently get a code reference. If you try to call it, though, it will give the following error message "Undefined subroutine &main::no_such_subroutine called at ...". It even remembers the name of the non-existent subroutine! Even stranger:

#!/usr/bin/perl -l

sub Foo::bar {'foo'}
*Foo::baz = \&::baz;

print Foo->bar;
print Foo->baz;
__END__
foo
Undefined subroutine &main::baz called at test.pl line 7.

Wait! What? I didn't call a subroutine. I called a method! What the hell is going on?

Ooooooohhhhhhhh.

In Catalyst, when you use a plugin, your application gets the methods because it inherits them. It doesn't install them into your namespace. Thus, when Pips3 used the authentication plugin, my constructor above, trying to grab a reference to &Pips3::user, was clearly stupid. What's worse, when I tried to restore it, I had a non-existent method overriding an existing one! How's that for a blunder? The fix is simple:

sub new {
    my ( $class, $user ) = @_;

    unless (defined wantarray) {
        croak('PIPTest::User->new($user) must not be called in void context');
    }
    my $self = bless {
        pips_user => Pips3->can('user'),
        cat_user  => Catalyst::Request->can('user'),
        user      => $user,
    } => $class;

    $self->_set_user($user);
    return $self;
}


Feature-ish

chromatic on 2008-06-26T17:14:33

This "strange behavior" is so that you can take a reference to an as-yet-undefined sub and call it later through the reference.